RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
run_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
7
8
9
10func create_run(options:Dictionary = {}) -> Dictionary:
11 var now = _now(options)
12 return {
13 "id": str(options.get("id", _make_run_id(options, now))),
14 "game_id": str(options.get("game_id", "")),
15 "mode": str(options.get("mode", "run")),
16 "status": "active",
17 "seed": int(options.get("seed", 0)),
18 "attempt": int(options.get("attempt", 1)),
19 "started_at": now,
20 "ended_at": 0,
21 "elapsed": 0.0,
22 "objectives": _normalize_objectives(options.get("objectives", [])),
23 "counters": _normalize_counts(options.get("counters", {})),
24 "collected": _normalize_counts(options.get("collected", {})),
25 "rewards": _normalize_counts(options.get("rewards", {})),
26 "result": {},
27 "metadata": options.get("metadata", {}).duplicate(true) if options.get("metadata", {}) is Dictionary else {}
28 }
29
30
31
32func is_active(run_state:Dictionary) -> bool:
33 return str(run_state.get("status", "")) == "active"
34
35
36
37func is_finished(run_state:Dictionary) -> bool:
38 return ["completed", "failed", "abandoned"].has(str(run_state.get("status", "")))
39
40
41
42func set_counter(run_state:Dictionary, key:String, value:int) -> Dictionary:
43 var next = run_state.duplicate(true)
44 var counters = next.get("counters", {}).duplicate(true) if next.get("counters", {}) is Dictionary else {}
45 counters[key] = value
46 next["counters"] = counters
47 return next
48
49
50
51func increment_counter(run_state:Dictionary, key:String, amount:int = 1) -> Dictionary:
52 var next = run_state.duplicate(true)
53 var counters = next.get("counters", {}).duplicate(true) if next.get("counters", {}) is Dictionary else {}
54 counters[key] = int(counters.get(key, 0)) + amount
55 next["counters"] = counters
56 return next
57
58
59
60func add_collected(run_state:Dictionary, collected:Dictionary) -> Dictionary:
61 var next = run_state.duplicate(true)
62 next["collected"] = _merge_counts(next.get("collected", {}), collected)
63 return next
64
65
66
67func set_objective_complete(run_state:Dictionary, objective_id:String, complete:bool = true) -> Dictionary:
68 var next = run_state.duplicate(true)
69 var objectives := []
70 for objective in next.get("objectives", []):
71 if not objective is Dictionary:
72 continue
73 var entry = objective.duplicate(true)
74 if str(entry.get("id", "")) == objective_id:
75 entry["complete"] = complete
76 objectives.append(entry)
77 next["objectives"] = objectives
78 return next
79
80
81
82func required_objectives_complete(run_state:Dictionary) -> bool:
83 for objective in run_state.get("objectives", []):
84 if objective is Dictionary and bool(objective.get("required", true)) and not bool(objective.get("complete", false)):
85 return false
86 return true
87
88
89
90func complete_run(run_state:Dictionary, rewards:Dictionary = {}, result:Dictionary = {}, options:Dictionary = {}) -> Dictionary:
91 var next = _finish(run_state, "completed", result, options)
92 next["rewards"] = _merge_counts(next.get("rewards", {}), rewards)
93 return next
94
95
96
97func fail_run(run_state:Dictionary, result:Dictionary = {}, options:Dictionary = {}) -> Dictionary:
98 return _finish(run_state, "failed", result, options)
99
100
101
102func abandon_run(run_state:Dictionary, options:Dictionary = {}) -> Dictionary:
103 var result = options.get("result", {}) if options.get("result", {}) is Dictionary else {}
104 result["rollback_collected"] = bool(options.get("rollback_collected", true))
105 result["message"] = str(options.get("message", result.get("message", "Run abandoned.")))
106 return _finish(run_state, "abandoned", result, options)
107
108
109
110func retry_run(run_state:Dictionary, options:Dictionary = {}) -> Dictionary:
111 var retry_options = {
112 "game_id": str(run_state.get("game_id", "")),
113 "mode": str(run_state.get("mode", "run")),
114 "seed": int(options.get("seed", run_state.get("seed", 0))),
115 "attempt": int(run_state.get("attempt", 1)) + 1,
116 "objectives": run_state.get("objectives", []),
117 "metadata": run_state.get("metadata", {})
118 }
119 for key in options.keys():
120 retry_options[key] = options[key]
121 return create_run(retry_options)
122
123
124
125func get_summary(run_state:Dictionary) -> Dictionary:
126 var objectives = run_state.get("objectives", [])
127 var required_count := 0
128 var required_complete := 0
129 for objective in objectives:
130 if not objective is Dictionary:
131 continue
132 if bool(objective.get("required", true)):
133 required_count += 1
134 if bool(objective.get("complete", false)):
135 required_complete += 1
136 return {
137 "id": str(run_state.get("id", "")),
138 "game_id": str(run_state.get("game_id", "")),
139 "mode": str(run_state.get("mode", "")),
140 "status": str(run_state.get("status", "")),
141 "attempt": int(run_state.get("attempt", 1)),
142 "elapsed": float(run_state.get("elapsed", 0.0)),
143 "required_objectives": required_count,
144 "completed_required_objectives": required_complete,
145 "counters": run_state.get("counters", {}).duplicate(true) if run_state.get("counters", {}) is Dictionary else {},
146 "collected": run_state.get("collected", {}).duplicate(true) if run_state.get("collected", {}) is Dictionary else {},
147 "rewards": run_state.get("rewards", {}).duplicate(true) if run_state.get("rewards", {}) is Dictionary else {},
148 "result": run_state.get("result", {}).duplicate(true) if run_state.get("result", {}) is Dictionary else {}
149 }
150
151
152
153func format_counts(counts:Dictionary, empty_label:String = "none") -> String:
154 var parts := []
155 var keys = counts.keys()
156 keys.sort()
157 for key in keys:
158 var amount = int(counts[key])
159 if amount <= 0:
160 continue
161 parts.append("%dx %s" % [amount, _labelize(str(key))])
162 return empty_label if parts.is_empty() else ", ".join(parts)
163
164
165
166func format_result_line(run_state:Dictionary) -> String:
167 var summary = get_summary(run_state)
168 var status = str(summary.get("status", "run")).capitalize()
169 var mode = _labelize(str(summary.get("mode", "run")))
170 var rewards = format_counts(summary.get("rewards", {}), "no rewards")
171 return "%s %s: %s" % [mode, status, rewards]
172
173
174func _finish(run_state:Dictionary, status:String, result:Dictionary = {}, options:Dictionary = {}) -> Dictionary:
175 var next = run_state.duplicate(true)
176 var now = _now(options)
177 next["status"] = status
178 next["ended_at"] = now
179 next["elapsed"] = float(options.get("elapsed", _elapsed(next, now)))
180 next["result"] = result.duplicate(true)
181 return next
182
183
184func _normalize_objectives(raw_objectives) -> Array:
185 var objectives := []
186 if not raw_objectives is Array:
187 return objectives
188 for raw_objective in raw_objectives:
189 if not raw_objective is Dictionary:
190 continue
191 var objective = raw_objective.duplicate(true)
192 objective["id"] = str(objective.get("id", ""))
193 objective["label"] = str(objective.get("label", objective.get("id", "")))
194 objective["required"] = bool(objective.get("required", true))
195 objective["complete"] = bool(objective.get("complete", false))
196 objectives.append(objective)
197 return objectives
198
199
200func _normalize_counts(raw_counts) -> Dictionary:
201 var counts := {}
202 if not raw_counts is Dictionary:
203 return counts
204 for key in raw_counts.keys():
205 counts[str(key)] = int(raw_counts[key])
206 return counts
207
208
209func _merge_counts(base_counts, added_counts) -> Dictionary:
210 var merged = _normalize_counts(base_counts)
211 if not added_counts is Dictionary:
212 return merged
213 for key in added_counts.keys():
214 merged[str(key)] = int(merged.get(str(key), 0)) + int(added_counts[key])
215 return merged
216
217
218func _now(options:Dictionary = {}) -> int:
219 if options.has("now"):
220 return int(options.get("now", 0))
221 return int(Time.get_unix_time_from_system())
222
223
224func _elapsed(run_state:Dictionary, now:int) -> float:
225 var started = int(run_state.get("started_at", now))
226 return max(0.0, float(now - started))
227
228
229func _make_run_id(options:Dictionary, now:int) -> String:
230 var game_id = str(options.get("game_id", "game"))
231 var mode = str(options.get("mode", "run"))
232 return "%s_%s_%d" % [game_id, mode, now]
233
234
235func _labelize(value:String) -> String:
236 return value.replace("_", " ").capitalize()
int
Increment a named counter and return a copied run state.
bool
Mark one objective complete/incomplete by id.