Video Game Design and Coding · PLECK lesson
PLECK lesson 120 · suggested week 24, day 5
Submit Seed Station with a save audit
Objective. Deliver a complete resource game and honest persistence evidence.
Godot Engine Standard desktop editor 4.7.2-stable, GDScript (Godot 4), Compatibility renderer. Headless Godot 4.7.2.stable.official.ed1daf0bf passed 106 assertions on the supplied scaffolds, including malformed JSON saves. Audio used the Dummy driver and a synthetic WAV, not a speaker. The visual editor, browser export, and signed-in submission were not tested. The desk still does not run a student's own upload.
About 55 minutes. The due date is chosen by a parent.
Reset, settings, deterministic state and a tested resource game
Suggested pace: Recall and predict 5 min; Read instruction and scaffold 12 min; Build or design the assigned artifact 25 min; Run acceptance tests 8 min; Explain and checkpoint 5 min.
Materials: Godot Standard 4.7.2 desktop editor (GDScript, Compatibility renderer), Local course folder and prior checkpoint, No paid assets or public account required.
Comes after VGC-L119.
Package the two-bed resource game with its data schema, transaction tests, save/load tests and reset policy. Include the source and original assets, but omit unrelated user data. Demonstrate a failed transaction that leaves inventory unchanged and a stop/relaunch that restores exactly the documented state. If using checkpoint-only persistence, say so explicitly and show how pickups reset without duplication. A reduced but coherent policy is better than claiming full persistence with missing flags.
Worked example
A reset matrix separates round score, saved progress and comfort preferences. Restarting a round may clear score while retaining a mute preference, if documented. Scaffold C-INVENTORY — Dictionary inventory with guarded spending Setup: Attach to Node InventoryLab for standalone testing. In a real level put the same functions on its inventory node and update UI after successful calls. Item IDs are short fixed strings, never a player’s name. A key represents one item type and its integer value is the quantity. ```gdscript extends Node var items: Dictionary = {"seed": 0, "water": 0} func add_item(id: String, amount: int = 1) -> bool: if not items.has(id) or amount <= 0: return false items[id] = int(items[id]) + amount return true func spend(id: String, amount: int = 1) -> bool: if not items.has(id) or amount <= 0 or int(items[id]) < amount: return false items[id] = int(items[id]) - amount return true func _ready() -> void: assert(add_item("seed", 2)) assert(spend("seed", 1)) assert(not spend("seed", 2)) assert(items["seed"] == 1) ``` Scaffold C-STATE-SAVE — Validated inventory and checkpoint data Setup: Attach to a Node named SaveData. This is a separate file from C-SAVE and only reads fixed known scalar fields. get_checkpoint_data returns plain data, not live node references. When applying a loaded checkpoint, the assignment maps checkpoint ID to a teacher-approved safe Marker2D, never to a filename supplied by the save. ```gdscript extends Node const PATH: String = "user://seed_station_v1.json" func defaults() -> Dictionary: return {"version": 1, "seed": 0, "water": 0, "checkpoint": "start"} func _bounded_count(value: Variant) -> int: if value is int or value is float: return clampi(int(value), 0, 99) return 0 func validate(raw: Variant) -> Dictionary: var clean := defaults() if not raw is Dictionary: return clean var data: Dictionary = raw if data.get("version", 0) != 1: return clean clean["seed"] = _bounded_count(data.get("seed", 0)) clean["water"] = _bounded_count(data.get("water", 0)) var checkpoint: Variant = data.get("checkpoint", "start") if checkpoint is String and checkpoint in ["start", "greenhouse"]: clean["checkpoint"] = checkpoint return clean func save_data(raw: Dictionary) -> bool: var file := FileAccess.open(PATH, FileAccess.WRITE) if file == null: return false file.store_string(JSON.stringify(validate(raw))) file.close() return true func load_data() -> Dictionary: if not FileAccess.file_exists(PATH): return defaults() var file := FileAccess.open(PATH, FileAccess.READ) if file == null: return defaults() var text: String = file.get_as_text() file.close() var parser := JSON.new() if parser.parse(text) != OK: return defaults() return validate(parser.data) ``` Scaffold C-CHECKPOINT — A safe checkpoint respawn Setup: Add this pattern to a separate Node2D level root with child Player using C-PLATFORM. Call set_checkpoint with a safe global position when a player reaches a checkpoint Area2D; call respawn when entering a fall zone. This code does not connect Area2D automatically; select the area body_entered signal in editor and call these methods from its handler after checking the player group. Respawn is deferred because a physics callback may be active. ```gdscript extends Node2D @onready var player: CharacterBody2D = $Player var checkpoint: Vector2 func _ready() -> void: checkpoint = player.global_position func set_checkpoint(safe_position: Vector2) -> void: checkpoint = safe_position func respawn() -> void: _apply_respawn.call_deferred() func _apply_respawn() -> void: player.global_position = checkpoint player.velocity = Vector2.ZERO ``` Scaffold C-WORLD-DATA — Consistent pickup/bed persistence with derived inventory Setup: Attach to a separate Node named WorldData. This is the default integrated Seed Station model after the scalar-save lab. Fixed pickup IDs are s1,s2,s3,w1,w2; fixed bed IDs are b1,b2. Manager records picked IDs only after a successful pickup and planted IDs only after validating both resource costs. Inventory is derived from these IDs, so reloading cannot keep quantities while respawning already-picked rewards. On load, hide/free picked objects, mark planted beds, set the known start/greenhouse marker, then refresh HUD. Version 1 scalar labs are intentionally not migrated: report a new-game fallback. ```gdscript extends Node const PATH: String = "user://seed_station_world_v2.json" func defaults() -> Dictionary: return {"version": 2, "picked": [], "planted": [], "seed": 0, "water": 0, "checkpoint": "start"} func _ids(raw: Variant, allowed: Array) -> Array[String]: var clean: Array[String] = [] if raw is Array: for value in raw: if value is String and value in allowed and value not in clean: clean.append(value) return clean func validate(raw: Variant) -> Dictionary: var result := defaults() if not raw is Dictionary: return result var data: Dictionary = raw if data.get("version", 0) != 2: return result var picked := _ids(data.get("picked", []), ["s1", "s2", "s3", "w1", "w2"]) var planted := _ids(data.get("planted", []), ["b1", "b2"]) var seed_total: int = 0 var water_total: int = 0 for id in picked: if id.begins_with("s"): seed_total += 1 else: water_total += 1 while planted.size() > mini(seed_total, water_total): planted.pop_back() result["picked"] = picked result["planted"] = planted result["seed"] = seed_total - planted.size() result["water"] = water_total - planted.size() result["checkpoint"] = "greenhouse" if "b1" in planted else "start" return result func save_data(raw: Dictionary) -> bool: var file := FileAccess.open(PATH, FileAccess.WRITE) if file == null: return false file.store_string(JSON.stringify(validate(raw))) file.close() return true func load_data() -> Dictionary: if not FileAccess.file_exists(PATH): return defaults() var file := FileAccess.open(PATH, FileAccess.READ) if file == null: return defaults() var text: String = file.get_as_text() file.close() var parser := JSON.new() if parser.parse(text) != OK: return defaults() return validate(parser.data) ```
Dictionary inventory with guarded spending
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-INVENTORY. Copy starts from this text. The desk does not run it.
Scene and node setup
Attach to Node InventoryLab for standalone testing. In a real level put the same functions on its inventory node and update UI after successful calls. Item IDs are short fixed strings, never a player’s name. A key represents one item type and its integer value is the quantity.
extends Node
var items: Dictionary = {"seed": 0, "water": 0}
func add_item(id: String, amount: int = 1) -> bool:
if not items.has(id) or amount <= 0:
return false
items[id] = int(items[id]) + amount
return true
func spend(id: String, amount: int = 1) -> bool:
if not items.has(id) or amount <= 0 or int(items[id]) < amount:
return false
items[id] = int(items[id]) - amount
return true
func _ready() -> void:
assert(add_item("seed", 2))
assert(spend("seed", 1))
assert(not spend("seed", 2))
assert(items["seed"] == 1)
Checks to run in the editor
- Spending more than owned returns false and does not change inventory.
- Unknown IDs and nonpositive amounts are rejected.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
Validated inventory and checkpoint data
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-STATE-SAVE. Copy starts from this text. The desk does not run it.
Scene and node setup
Attach to a Node named SaveData. This is a separate file from C-SAVE and only reads fixed known scalar fields. get_checkpoint_data returns plain data, not live node references. When applying a loaded checkpoint, the assignment maps checkpoint ID to a teacher-approved safe Marker2D, never to a filename supplied by the save.
extends Node
const PATH: String = "user://seed_station_v1.json"
func defaults() -> Dictionary:
return {"version": 1, "seed": 0, "water": 0, "checkpoint": "start"}
func _bounded_count(value: Variant) -> int:
if value is int or value is float:
return clampi(int(value), 0, 99)
return 0
func validate(raw: Variant) -> Dictionary:
var clean := defaults()
if not raw is Dictionary:
return clean
var data: Dictionary = raw
if data.get("version", 0) != 1:
return clean
clean["seed"] = _bounded_count(data.get("seed", 0))
clean["water"] = _bounded_count(data.get("water", 0))
var checkpoint: Variant = data.get("checkpoint", "start")
if checkpoint is String and checkpoint in ["start", "greenhouse"]:
clean["checkpoint"] = checkpoint
return clean
func save_data(raw: Dictionary) -> bool:
var file := FileAccess.open(PATH, FileAccess.WRITE)
if file == null:
return false
file.store_string(JSON.stringify(validate(raw)))
file.close()
return true
func load_data() -> Dictionary:
if not FileAccess.file_exists(PATH):
return defaults()
var file := FileAccess.open(PATH, FileAccess.READ)
if file == null:
return defaults()
var text: String = file.get_as_text()
file.close()
var parser := JSON.new()
if parser.parse(text) != OK:
return defaults()
return validate(parser.data)
Checks to run in the editor
- Missing file returns defaults.
- Negative count becomes zero; count 1000 becomes 99.
- Unknown checkpoint becomes start.
- Malformed JSON, arrays and unsupported versions return safe defaults.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
A safe checkpoint respawn
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-CHECKPOINT. Copy starts from this text. The desk does not run it.
Scene and node setup
Add this pattern to a separate Node2D level root with child Player using C-PLATFORM. Call set_checkpoint with a safe global position when a player reaches a checkpoint Area2D; call respawn when entering a fall zone. This code does not connect Area2D automatically; select the area body_entered signal in editor and call these methods from its handler after checking the player group. Respawn is deferred because a physics callback may be active.
extends Node2D
@onready var player: CharacterBody2D = $Player
var checkpoint: Vector2
func _ready() -> void:
checkpoint = player.global_position
func set_checkpoint(safe_position: Vector2) -> void:
checkpoint = safe_position
func respawn() -> void:
_apply_respawn.call_deferred()
func _apply_respawn() -> void:
player.global_position = checkpoint
player.velocity = Vector2.ZERO
Checks to run in the editor
- A fall returns to last safe point, not origin.
- Player has no leftover falling velocity on return.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
Consistent pickup/bed persistence with derived inventory
GDScript (Godot 4, not Scratch, Python, JavaScript or C#). Scaffold C-WORLD-DATA. Copy starts from this text. The desk does not run it.
Scene and node setup
Attach to a separate Node named WorldData. This is the default integrated Seed Station model after the scalar-save lab. Fixed pickup IDs are s1,s2,s3,w1,w2; fixed bed IDs are b1,b2. Manager records picked IDs only after a successful pickup and planted IDs only after validating both resource costs. Inventory is derived from these IDs, so reloading cannot keep quantities while respawning already-picked rewards. On load, hide/free picked objects, mark planted beds, set the known start/greenhouse marker, then refresh HUD. Version 1 scalar labs are intentionally not migrated: report a new-game fallback.
extends Node
const PATH: String = "user://seed_station_world_v2.json"
func defaults() -> Dictionary:
return {"version": 2, "picked": [], "planted": [], "seed": 0, "water": 0, "checkpoint": "start"}
func _ids(raw: Variant, allowed: Array) -> Array[String]:
var clean: Array[String] = []
if raw is Array:
for value in raw:
if value is String and value in allowed and value not in clean:
clean.append(value)
return clean
func validate(raw: Variant) -> Dictionary:
var result := defaults()
if not raw is Dictionary:
return result
var data: Dictionary = raw
if data.get("version", 0) != 2:
return result
var picked := _ids(data.get("picked", []), ["s1", "s2", "s3", "w1", "w2"])
var planted := _ids(data.get("planted", []), ["b1", "b2"])
var seed_total: int = 0
var water_total: int = 0
for id in picked:
if id.begins_with("s"):
seed_total += 1
else:
water_total += 1
while planted.size() > mini(seed_total, water_total):
planted.pop_back()
result["picked"] = picked
result["planted"] = planted
result["seed"] = seed_total - planted.size()
result["water"] = water_total - planted.size()
result["checkpoint"] = "greenhouse" if "b1" in planted else "start"
return result
func save_data(raw: Dictionary) -> bool:
var file := FileAccess.open(PATH, FileAccess.WRITE)
if file == null:
return false
file.store_string(JSON.stringify(validate(raw)))
file.close()
return true
func load_data() -> Dictionary:
if not FileAccess.file_exists(PATH):
return defaults()
var file := FileAccess.open(PATH, FileAccess.READ)
if file == null:
return defaults()
var text: String = file.get_as_text()
file.close()
var parser := JSON.new()
if parser.parse(text) != OK:
return defaults()
return validate(parser.data)
Checks to run in the editor
- Picked s1,s2,w1 and planted b1 yields seed1,water0,greenhouse.
- Duplicate s1 entries do not add extra inventory.
- No collected IDs plus two planted IDs cleans to no planted beds.
- Reload removes collected pickups and restores planted-bed state through the manager.
Native Godot 4.7.2.stable.official.ed1daf0bf headless compile and representative node-tree/runtime tests passed; see native-qa-results.json. Visual UI, audible output, exports and website submission still need separate checks.
Submit Seed Station with a save audit — evidence task
- 1. Restore the source and execute transaction, completion, save, load, invalid-data and reset tests.
- 2. Submit schema and known limitations.
Deliverable. Seed Station source artifact, schema, transaction table and persistence audit.
Acceptance checks
- VGC-L120-T1. All claimed persisted fields restore correctly.
- VGC-L120-T2. No resource duplication appears across the tested save/load cycle.
Understanding question. What should the teacher assess if checkpoint-only persistence is chosen?
Include a downloadable project ZIP with project.godot, .tscn, .gd and original asset files when code changes. A private, teacher-accessible download link to that ZIP is equivalent. Screenshots or a playthrough do not replace editable source.
If this is still shaky. If full world persistence is too large, use the documented checkpoint-only reset policy. Do not claim unsaved flags persist. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
Optional extra. Add one small validated setting using a separate save field or configuration file. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.
- Godot step by step: nodes, scenes, GDScript, input, signals
Free public documentation, no account. Same class help is available within the desktop editor.
· Opening this link does not submit the lesson. - Saving games: FileAccess and JSON
Free public documentation, no account.
· Opening this link does not submit the lesson.
Name ____________________ Date ____________________