Video Game Design and Coding

Week 24 packet

Student instructions, tasks, and checks. The answer key is not in this packet. Links stay as text. This page does not embed a clip or run a file.

# Week 24 — Persistence policies and complete data projects

## VGC-L116 — Define New Game versus Continue

**Objective:** Make destructive reset a deliberate, reversible user choice.

**Time:** 55 minutes

### Instruction
Continue restores valid saved progress; New Game resets the documented progress fields. The controls must not be indistinguishable. For this course, show a confirmation before replacing a valid course save, and preserve a local backup if practical. Do not delete unrelated files. Decide whether comfort settings survive New Game independently of progress. A cancelled confirmation must leave state unchanged. Your implementation should affect only your project’s course-specific save path.

### 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-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-DIALOGUE — Data-driven branching dialogue without personal data
Setup: Create Control root Dialogue with VBoxContainer Box containing Label Line and Buttons A and B. Set Box anchors to full rect; Line autowrap if needed. Attach script to Dialogue. Data keys are fictional scene IDs. All three screens can fit in 640x360. This is a self-contained example; replace story text in the assignment.
```gdscript
extends Control
var page: String = "start"
var story: Dictionary = {
    "start": {"text": "Two paths enter the archive.", "choices": [["Ask the keeper", "keeper"], ["Read a sign", "sign"]]},
    "keeper": {"text": "The keeper offers a map.", "choices": [["Return", "start"]]},
    "sign": {"text": "Quiet room to the east.", "choices": [["Return", "start"]]}
}
func _ready() -> void:
    $Box/A.pressed.connect(_choose.bind(0))
    $Box/B.pressed.connect(_choose.bind(1))
    _show()
func _show() -> void:
    var entry: Dictionary = story[page]
    $Box/Line.text = entry["text"]
    var buttons: Array[Button] = [$Box/A, $Box/B]
    for i in range(buttons.size()):
        buttons[i].visible = i < entry["choices"].size()
        if buttons[i].visible:
            buttons[i].text = entry["choices"][i][0]
    $Box/A.grab_focus()
func _choose(index: int) -> void:
    var choices: Array = story[page]["choices"]
    if index < 0 or index >= choices.size():
        return
    page = str(choices[index][1])
    _show()
```

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)
```

### Assignment
1. Implement distinct Continue and New Game controls with a cancelable reset confirmation.
2. Test cancel, confirm and Continue from a valid save.

### Acceptance checks
- Cancel preserves saved progress.
- Confirm resets only documented course progress and Continue uses the resulting state.

**Submit:** Reset/continue UI and three-case evidence.

**Question:** Why might comfort settings have a different reset policy from game progress?

**Support:** 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 extension:** 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.

**Optional free reading:** [Godot step by step: nodes, scenes, GDScript, input, signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/index.html), [Saving games: FileAccess and JSON](https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html)

---

## VGC-L117 — Add a versioned schema change deliberately

**Objective:** Handle an older save without guessing field meaning.

**Time:** 55 minutes

### Instruction
A schema version explains how stored fields should be interpreted. Add one new field, such as muted, with a Boolean type and default false. Choose either a small explicit migration from version one or a clearly communicated fallback to a fresh state; do not reinterpret unknown versions arbitrarily. The simpler course extension is accepting the old known version and filling the new field’s default. Test old, new and unsupported version inputs. Save-data evolution should never execute code from the file.

### 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-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-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)
```

### Assignment
1. Add one validated field and document old-version behavior.
2. Test old version, current version and an unsupported future version.

### Acceptance checks
- The new field has a safe default.
- Unsupported versions follow the documented fallback instead of crashing.

**Submit:** Schema change note, validator source and three-version tests.

**Question:** Why not assume a larger version number means compatible data?

**Support:** 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 extension:** 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.

**Optional free reading:** [Godot step by step: nodes, scenes, GDScript, input, signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/index.html), [Saving games: FileAccess and JSON](https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html)

---

## VGC-L118 — Audit conservation of resources

**Objective:** Use an invariant to catch duplication and partial transactions.

**Time:** 55 minutes

### Instruction
An invariant is a relationship that should remain true after every legal action. For Seed Station, total seeds available equals uncollected seeds plus inventory seeds plus seeds already spent, provided no new seeds spawn. Write the same relationship for water. Trace collection, failed planting, successful planting and restore under your chosen save policy. If totals increase unexpectedly, a pickup or load path probably duplicates state. This checks behavior across several systems rather than checking only one function.

### 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-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)
```

### Assignment
1. Write seed and water conservation tables for four event types.
2. Repair or explain any mismatch under the documented persistence policy.

### Acceptance checks
- Failed planting preserves totals.
- Save/load does not create additional resources.

**Submit:** Invariant table and tested Seed Station checkpoint.

**Question:** What does an unexpected increase in the conserved total suggest?

**Support:** 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 extension:** 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.

**Optional free reading:** [Godot step by step: nodes, scenes, GDScript, input, signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/index.html), [Saving games: FileAccess and JSON](https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html)

---

## VGC-L119 — Explain browser save limitations and local backup

**Objective:** Distinguish source backups from player progress storage.

**Time:** 55 minutes

### Instruction
A source ZIP backs up the game’s code and assets, not automatically its user:// progress. Desktop saves usually live outside the project folder. Browser exports use browser-managed storage that may be unavailable or cleared in private browsing, embedded contexts or storage cleanup. Do not promise permanent progress in a web build without testing. The required assessment uses the local editor and course-only saves. Write how a teacher can reproduce a save test without accessing your unrelated personal files.

### 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.

### Assignment
1. Add source-versus-progress backup notes to the readme.
2. Write a reproducible local save-test procedure and list browser limitations.

### Acceptance checks
- The readme does not claim the project ZIP contains user progress.
- No procedure requests unrelated private data.

**Submit:** Persistence readme and local test procedure.

**Question:** Does a source-code backup guarantee a player’s saved progress is backed up?

**Support:** 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 extension:** 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.

**Optional free reading:** [Godot step by step: nodes, scenes, GDScript, input, signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/index.html), [Saving games: FileAccess and JSON](https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html)

---

## VGC-L120 — Submit Seed Station with a save audit

**Objective:** Deliver a complete resource game and honest persistence evidence.

**Time:** 55 minutes

### Instruction
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)
```

### Assignment
1. Restore the source and execute transaction, completion, save, load, invalid-data and reset tests.
2. Submit schema and known limitations.

### Acceptance checks
- All claimed persisted fields restore correctly.
- No resource duplication appears across the tested save/load cycle.

**Submit:** Seed Station source artifact, schema, transaction table and persistence audit.

**Question:** What should the teacher assess if checkpoint-only persistence is chosen?

**Support:** 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 extension:** 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.

**Optional free reading:** [Godot step by step: nodes, scenes, GDScript, input, signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/index.html), [Saving games: FileAccess and JSON](https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html)

---