feat: add simulation action definitions

This commit is contained in:
2026-07-05 13:35:33 +02:00
parent 12f9ba616f
commit f816f3976a
32 changed files with 608 additions and 113 deletions
+54 -56
View File
@@ -1,20 +1,20 @@
class_name SimNPC
extends RefCounted
const TASK_STATE_IDLE := "idle"
const TASK_STATE_TRAVELING := "traveling"
const TASK_STATE_WORKING := "working"
const TASK_STATE_COMPLETE := "complete"
const TASK_STATE_IDLE := &"idle"
const TASK_STATE_TRAVELING := &"traveling"
const TASK_STATE_WORKING := &"working"
const TASK_STATE_COMPLETE := &"complete"
var id: int
var npc_name: String
var profession: String
var profession: StringName
var hunger: float
var energy: float
var strength: float
var intelligence: float
var current_task: String = "idle"
var task_state: String = TASK_STATE_IDLE
var current_task: StringName = SimulationIds.ACTION_IDLE
var task_state: StringName = TASK_STATE_IDLE
var position: Vector3
var is_starving := false
@@ -26,14 +26,14 @@ var task_duration := 0.0
var task_progress := 0.0
var task_complete := true
var target_id: StringName = &""
var last_task := ""
var last_task: StringName
var random_source: RandomNumberGenerator
var debug_logs := true
func _init(
_id: int,
_npc_name: String,
_profession: String,
_profession: StringName,
_strength: float,
_intelligence: float,
_random_source: RandomNumberGenerator = null
@@ -87,7 +87,7 @@ func update_needs(village: SimVillage) -> void:
TASK_STATE_TRAVELING:
energy -= 2.0
TASK_STATE_WORKING:
if current_task != "rest":
if current_task != SimulationIds.ACTION_REST:
energy -= 3.0
TASK_STATE_COMPLETE:
energy -= 0.25
@@ -107,7 +107,7 @@ func update_needs(village: SimVillage) -> void:
func die_from_starvation() -> void:
is_dead = true
current_task = "dead"
current_task = SimulationIds.ACTION_DEAD
task_state = TASK_STATE_IDLE
task_progress = 0.0
task_duration = 0.0
@@ -122,110 +122,97 @@ func choose_new_task(village: SimVillage) -> void:
if is_starving:
if village.food > 0:
set_task("eat", 1.0)
set_task(SimulationIds.ACTION_EAT, 1.0)
else:
set_task("gather_food", 4.0)
set_task(SimulationIds.ACTION_GATHER_FOOD, 4.0)
return
if hunger > 75.0:
if village.food > 0:
set_task("eat", 2.0)
set_task(SimulationIds.ACTION_EAT)
else:
set_task("gather_food", 5.0)
set_task(SimulationIds.ACTION_GATHER_FOOD)
return
if energy < 25.0:
set_task("rest", 4.0)
set_task(SimulationIds.ACTION_REST)
return
var best_task := choose_best_work_task(village)
match best_task:
"gather_food":
set_task("gather_food", 5.0)
"gather_wood":
set_task("gather_wood", 5.0)
"patrol":
set_task("patrol", 6.0)
"study":
set_task("study", 6.0)
_:
set_task("wander", 4.0)
set_task(choose_best_work_task(village))
func should_eat_immediately_after_task() -> bool:
return current_task == "gather_food" and is_starving and not is_dead
return (
current_task == SimulationIds.ACTION_GATHER_FOOD
and is_starving
and not is_dead
)
func choose_best_work_task(village: SimVillage) -> String:
func choose_best_work_task(village: SimVillage) -> StringName:
var food_score := calculate_task_score(
"gather_food",
SimulationIds.ACTION_GATHER_FOOD,
village.food_priority,
village.food,
15.0,
30.0,
3.0,
1.5,
"farmer"
1.5
)
var wood_score := calculate_task_score(
"gather_wood",
SimulationIds.ACTION_GATHER_WOOD,
village.wood_priority,
village.wood,
10.0,
20.0,
2.5,
1.0,
"woodcutter"
1.0
)
var safety_score := calculate_task_score(
"patrol",
SimulationIds.ACTION_PATROL,
village.safety_priority,
village.safety,
30.0,
50.0,
3.0,
1.5,
"guard"
1.5
)
var knowledge_score := calculate_task_score(
"study",
SimulationIds.ACTION_STUDY,
village.knowledge_priority,
village.knowledge,
10.0,
25.0,
1.5,
0.75,
"scholar"
0.75
)
var best_task := "gather_food"
var best_task := SimulationIds.ACTION_GATHER_FOOD
var best_score := food_score
if wood_score > best_score:
best_task = "gather_wood"
best_task = SimulationIds.ACTION_GATHER_WOOD
best_score = wood_score
if safety_score > best_score:
best_task = "patrol"
best_task = SimulationIds.ACTION_PATROL
best_score = safety_score
if knowledge_score > best_score:
best_task = "study"
best_task = SimulationIds.ACTION_STUDY
best_score = knowledge_score
return best_task
func calculate_task_score(
task_name: String,
action_id: StringName,
base_priority: float,
resource_amount: float,
critical_threshold: float,
low_threshold: float,
critical_bonus: float,
low_bonus: float,
preferred_profession: String
low_bonus: float
) -> float:
var score := base_priority
@@ -234,10 +221,15 @@ func calculate_task_score(
elif resource_amount < low_threshold:
score += low_bonus
if profession == preferred_profession:
var definition := SimulationDefinitions.get_action(action_id)
if (
definition != null
and not definition.preferred_profession_id.is_empty()
and profession == definition.preferred_profession_id
):
score += 2.0
if task_name == last_task:
if action_id == last_task:
score -= 1.5
score += random_source.randf_range(0.0, 0.5)
@@ -247,16 +239,22 @@ func calculate_task_score(
"[SimNPC] ",
npc_name,
" score ",
task_name,
action_id,
" = ",
score
)
return score
func set_task(task: String, duration: float) -> void:
current_task = task
task_duration = duration
func set_task(action_id: StringName, duration: float = -1.0) -> void:
var definition := SimulationDefinitions.get_action(action_id)
if definition == null:
push_error("SimNPC: unknown action_id '%s'" % action_id)
return
current_task = action_id
task_duration = (
duration if duration >= 0.0 else definition.default_duration
)
task_progress = 0.0
task_complete = false
task_state = TASK_STATE_TRAVELING
+7 -7
View File
@@ -101,15 +101,15 @@ func apply_npc_task(npc: SimNPC) -> void:
productivity = 0.5
match npc.current_task:
"gather_food":
SimulationIds.ACTION_GATHER_FOOD:
food += 2.0 * productivity
"gather_wood":
SimulationIds.ACTION_GATHER_WOOD:
wood += 2.0 * productivity
"patrol":
SimulationIds.ACTION_PATROL:
safety += 1.0 * productivity
"study":
SimulationIds.ACTION_STUDY:
knowledge += 1.0 * productivity
"eat":
SimulationIds.ACTION_EAT:
if food > 0:
food -= 1.0
npc.hunger = max(npc.hunger - 55.0, 0.0)
@@ -118,14 +118,14 @@ func apply_npc_task(npc: SimNPC) -> void:
else:
npc.hunger = min(npc.hunger + 5.0, 100.0)
npc.is_starving = npc.hunger >= 90.0
"rest":
SimulationIds.ACTION_REST:
if npc.is_starving:
npc.energy = min(npc.energy + 2.0, 100.0)
elif npc.hunger > 75.0:
npc.energy = min(npc.energy + 12.0, 100.0)
else:
npc.energy = min(npc.energy + 35.0, 100.0)
"wander":
SimulationIds.ACTION_WANDER:
safety -= 0.2
food = max(food, 0.0)
+35 -15
View File
@@ -1,6 +1,10 @@
extends Node
signal npc_task_changed(npc: SimNPC, old_task: String, new_task: String)
signal npc_task_changed(
npc: SimNPC,
old_task: StringName,
new_task: StringName
)
signal village_changed(village: SimVillage)
signal npc_died(npc: SimNPC)
@@ -20,16 +24,14 @@ var names := [
"Amina", "Tarik", "Jasmin"
]
var professions := [
"farmer",
"woodcutter",
"guard",
"scholar",
"wanderer"
]
func _ready() -> void:
add_to_group("simulation_manager")
var definition_errors := SimulationDefinitions.validate()
if not definition_errors.is_empty():
for error in definition_errors:
push_error("SimulationManager: " + error)
set_process(false)
return
clock = SimulationClock.new(tick_interval)
village.debug_logs = debug_logs
village.update_modifiers()
@@ -45,13 +47,17 @@ func _process(delta: float) -> void:
simulate_tick()
func generate_npcs() -> void:
var profession_ids := SimulationDefinitions.get_profession_ids()
for i in range(names.size()):
var npc_random := _create_random_source(i, 0)
var profession_index := npc_random.randi_range(0, professions.size() - 1)
var profession_index := npc_random.randi_range(
0,
profession_ids.size() - 1
)
var npc := SimNPC.new(
i,
names[i],
professions[profession_index],
profession_ids[profession_index],
npc_random.randf_range(1.0, 10.0),
npc_random.randf_range(1.0, 10.0),
npc_random
@@ -138,7 +144,10 @@ func simulate_tick() -> void:
elif debug_logs:
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": target reservation was invalid")
release_npc_reservation(npc.id)
elif completed_task not in ["gather_food", "gather_wood"]:
elif completed_task not in [
SimulationIds.ACTION_GATHER_FOOD,
SimulationIds.ACTION_GATHER_WOOD
]:
village.apply_npc_task(npc)
elif debug_logs:
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": no resource target")
@@ -148,10 +157,10 @@ func simulate_tick() -> void:
npc.task_complete = true
npc.task_state = SimNPC.TASK_STATE_IDLE
npc.last_task = completed_task
npc.current_task = "idle"
npc.current_task = SimulationIds.ACTION_IDLE
if needs_immediate_food_after_gather and village.food > 0:
npc.set_task("eat", 1.0)
npc.set_task(SimulationIds.ACTION_EAT, 1.0)
npc_task_changed.emit(npc, completed_task, npc.current_task)
if debug_logs:
@@ -244,7 +253,7 @@ func notify_npc_navigation_failed(npc_id: int) -> void:
var failed_task := npc.current_task
release_npc_reservation(npc.id)
npc.last_task = failed_task
npc.set_task("wander", 2.0)
npc.set_task(SimulationIds.ACTION_WANDER, 2.0)
npc_task_changed.emit(npc, failed_task, npc.current_task)
if debug_logs:
@@ -261,6 +270,17 @@ func register_loaded_resource_nodes() -> void:
func register_resource_node(node: ResourceNode) -> bool:
if node == null or node.node_id.is_empty():
return false
var action_definition := SimulationDefinitions.get_action(node.action_id)
if (
action_definition == null
or action_definition.target_type != SimulationIds.TARGET_RESOURCE
or action_definition.resource_action_id != node.action_id
):
push_error(
"SimulationManager: ResourceNode '%s' has invalid action_id '%s'"
% [node.node_id, node.action_id]
)
return false
var resource_state := get_resource_state(node.node_id)
if resource_state == null:
resource_state = ResourceStateRecord.create_from_node(node)
@@ -0,0 +1,30 @@
class_name ActionDefinition
extends Resource
@export var action_id: StringName
@export var display_name: String
@export_range(0.0, 1000.0, 0.1) var default_duration := 1.0
@export var preferred_profession_id: StringName
@export var target_type: StringName = SimulationIds.TARGET_ACTIVITY
@export var resource_action_id: StringName
func validate() -> Array[String]:
var errors: Array[String] = []
if action_id.is_empty():
errors.append("action_id is empty")
if display_name.is_empty():
errors.append("display_name is empty for '%s'" % action_id)
if default_duration <= 0.0:
errors.append("default_duration must be positive for '%s'" % action_id)
if target_type not in [
SimulationIds.TARGET_RESOURCE,
SimulationIds.TARGET_ACTIVITY,
SimulationIds.TARGET_FREE
]:
errors.append("target_type '%s' is invalid for '%s'" % [target_type, action_id])
if (
target_type == SimulationIds.TARGET_RESOURCE
and resource_action_id.is_empty()
):
errors.append("resource_action_id is required for '%s'" % action_id)
return errors
@@ -0,0 +1 @@
uid://b1utbqqel3pyw
@@ -0,0 +1,13 @@
class_name ProfessionDefinition
extends Resource
@export var profession_id: StringName
@export var display_name: String
func validate() -> Array[String]:
var errors: Array[String] = []
if profession_id.is_empty():
errors.append("profession_id is empty")
if display_name.is_empty():
errors.append("display_name is empty for '%s'" % profession_id)
return errors
@@ -0,0 +1 @@
uid://cimqtsrs3huq0
@@ -0,0 +1,97 @@
class_name SimulationDefinitions
extends RefCounted
const ACTION_PATHS := [
"res://simulation/definitions/actions/gather_food.tres",
"res://simulation/definitions/actions/gather_wood.tres",
"res://simulation/definitions/actions/patrol.tres",
"res://simulation/definitions/actions/study.tres",
"res://simulation/definitions/actions/eat.tres",
"res://simulation/definitions/actions/rest.tres",
"res://simulation/definitions/actions/wander.tres"
]
const PROFESSION_PATHS := [
"res://simulation/definitions/professions/farmer.tres",
"res://simulation/definitions/professions/woodcutter.tres",
"res://simulation/definitions/professions/guard.tres",
"res://simulation/definitions/professions/scholar.tres",
"res://simulation/definitions/professions/wanderer.tres"
]
static func get_actions() -> Array[ActionDefinition]:
var definitions: Array[ActionDefinition] = []
for path in ACTION_PATHS:
var definition := load(path) as ActionDefinition
if definition != null:
definitions.append(definition)
return definitions
static func get_professions() -> Array[ProfessionDefinition]:
var definitions: Array[ProfessionDefinition] = []
for path in PROFESSION_PATHS:
var definition := load(path) as ProfessionDefinition
if definition != null:
definitions.append(definition)
return definitions
static func get_action(action_id: StringName) -> ActionDefinition:
for definition in get_actions():
if definition.action_id == action_id:
return definition
return null
static func get_profession(
profession_id: StringName
) -> ProfessionDefinition:
for definition in get_professions():
if definition.profession_id == profession_id:
return definition
return null
static func get_profession_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for definition in get_professions():
ids.append(definition.profession_id)
return ids
static func validate() -> Array[String]:
var errors: Array[String] = []
if get_actions().size() != ACTION_PATHS.size():
errors.append("One or more ActionDefinition resources failed to load")
if get_professions().size() != PROFESSION_PATHS.size():
errors.append("One or more ProfessionDefinition resources failed to load")
var action_ids := {}
for definition in get_actions():
for error in definition.validate():
errors.append("ActionDefinition: " + error)
if action_ids.has(definition.action_id):
errors.append("Duplicate action_id '%s'" % definition.action_id)
action_ids[definition.action_id] = true
var profession_ids := {}
for definition in get_professions():
for error in definition.validate():
errors.append("ProfessionDefinition: " + error)
if profession_ids.has(definition.profession_id):
errors.append("Duplicate profession_id '%s'" % definition.profession_id)
profession_ids[definition.profession_id] = true
for definition in get_actions():
if (
not definition.preferred_profession_id.is_empty()
and not profession_ids.has(definition.preferred_profession_id)
):
errors.append(
"Action '%s' references unknown profession '%s'"
% [definition.action_id, definition.preferred_profession_id]
)
if (
definition.target_type == SimulationIds.TARGET_RESOURCE
and not action_ids.has(definition.resource_action_id)
):
errors.append(
"Action '%s' references unknown resource action '%s'"
% [definition.action_id, definition.resource_action_id]
)
return errors
@@ -0,0 +1 @@
uid://v1sohi6bbuga
+25
View File
@@ -0,0 +1,25 @@
class_name SimulationIds
extends RefCounted
const ACTION_IDLE := &"idle"
const ACTION_DEAD := &"dead"
const ACTION_GATHER_FOOD := &"gather_food"
const ACTION_GATHER_WOOD := &"gather_wood"
const ACTION_PATROL := &"patrol"
const ACTION_STUDY := &"study"
const ACTION_EAT := &"eat"
const ACTION_REST := &"rest"
const ACTION_WANDER := &"wander"
const PROFESSION_FARMER := &"farmer"
const PROFESSION_WOODCUTTER := &"woodcutter"
const PROFESSION_GUARD := &"guard"
const PROFESSION_SCHOLAR := &"scholar"
const PROFESSION_WANDERER := &"wanderer"
const TARGET_RESOURCE := &"resource"
const TARGET_ACTIVITY := &"activity"
const TARGET_FREE := &"free"
const RESOURCE_FOOD := &"food"
const RESOURCE_WOOD := &"wood"
@@ -0,0 +1 @@
uid://brf7tywgsj6v7
+10
View File
@@ -0,0 +1,10 @@
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
action_id = &"eat"
display_name = "Eat"
default_duration = 2.0
target_type = &"activity"
@@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
action_id = &"gather_food"
display_name = "Gather Food"
default_duration = 5.0
preferred_profession_id = &"farmer"
target_type = &"resource"
resource_action_id = &"gather_food"
@@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
action_id = &"gather_wood"
display_name = "Gather Wood"
default_duration = 5.0
preferred_profession_id = &"woodcutter"
target_type = &"resource"
resource_action_id = &"gather_wood"
@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
action_id = &"patrol"
display_name = "Patrol"
default_duration = 6.0
preferred_profession_id = &"guard"
target_type = &"activity"
+10
View File
@@ -0,0 +1,10 @@
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
action_id = &"rest"
display_name = "Rest"
default_duration = 4.0
target_type = &"activity"
+11
View File
@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
action_id = &"study"
display_name = "Study"
default_duration = 6.0
preferred_profession_id = &"scholar"
target_type = &"activity"
@@ -0,0 +1,10 @@
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
action_id = &"wander"
display_name = "Wander"
default_duration = 4.0
target_type = &"free"
@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
profession_id = &"farmer"
display_name = "Farmer"
@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
profession_id = &"guard"
display_name = "Guard"
@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
profession_id = &"scholar"
display_name = "Scholar"
@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
profession_id = &"wanderer"
display_name = "Wanderer"
@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
profession_id = &"woodcutter"
display_name = "Woodcutter"
+26 -8
View File
@@ -13,13 +13,13 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
"schema_version": SCHEMA_VERSION,
"id": npc.id,
"name": npc.npc_name,
"profession": npc.profession,
"profession": String(npc.profession),
"hunger": npc.hunger,
"energy": npc.energy,
"strength": npc.strength,
"intelligence": npc.intelligence,
"current_task": npc.current_task,
"task_state": npc.task_state,
"current_task": String(npc.current_task),
"task_state": String(npc.task_state),
"position": [npc.position.x, npc.position.y, npc.position.z],
"is_starving": npc.is_starving,
"starvation_ticks": npc.starvation_ticks,
@@ -29,7 +29,7 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
"task_progress": npc.task_progress,
"task_complete": npc.task_complete,
"target_id": String(npc.target_id),
"last_task": npc.last_task,
"last_task": String(npc.last_task),
"random_seed": str(npc.random_source.seed),
"random_state": str(npc.random_source.state)
})
@@ -48,6 +48,24 @@ static func from_dictionary(record_data: Dictionary) -> NPCStateRecord:
var saved_position = record_data["position"]
if not saved_position is Array or saved_position.size() != 3:
return null
var profession_id := StringName(record_data["profession"])
if SimulationDefinitions.get_profession(profession_id) == null:
return null
var action_id := StringName(record_data["current_task"])
if (
action_id not in [
SimulationIds.ACTION_IDLE,
SimulationIds.ACTION_DEAD
]
and SimulationDefinitions.get_action(action_id) == null
):
return null
var last_action_id := StringName(record_data["last_task"])
if (
not last_action_id.is_empty()
and SimulationDefinitions.get_action(last_action_id) == null
):
return null
return NPCStateRecord.new(record_data)
func restore(debug_logs: bool) -> SimNPC:
@@ -56,7 +74,7 @@ func restore(debug_logs: bool) -> SimNPC:
var npc := SimNPC.new(
int(data["id"]),
String(data["name"]),
String(data["profession"]),
StringName(data["profession"]),
float(data["strength"]),
float(data["intelligence"]),
random_source
@@ -64,8 +82,8 @@ func restore(debug_logs: bool) -> SimNPC:
var saved_position: Array = data["position"]
npc.hunger = float(data["hunger"])
npc.energy = float(data["energy"])
npc.current_task = String(data["current_task"])
npc.task_state = String(data["task_state"])
npc.current_task = StringName(data["current_task"])
npc.task_state = StringName(data["task_state"])
npc.position = Vector3(
float(saved_position[0]),
float(saved_position[1]),
@@ -79,7 +97,7 @@ func restore(debug_logs: bool) -> SimNPC:
npc.task_progress = float(data["task_progress"])
npc.task_complete = bool(data["task_complete"])
npc.target_id = StringName(data["target_id"])
npc.last_task = String(data["last_task"])
npc.last_task = StringName(data["last_task"])
npc.random_source.state = String(data["random_state"]).to_int()
npc.debug_logs = debug_logs
return npc
+6
View File
@@ -41,6 +41,12 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
return null
if String(record_data["node_id"]).is_empty():
return null
var action_id := StringName(record_data["action_id"])
if (
not action_id.is_empty()
and SimulationDefinitions.get_action(action_id) == null
):
return null
return ResourceStateRecord.new(record_data)
static func _migrate_v1(legacy_data: Dictionary) -> Dictionary: