Video Game Design and Coding

Week 31 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 31 — Core production

## VGC-L151 — Harden input and boundary behavior

**Objective:** Handle held, repeated and unavailable actions correctly.

**Time:** 55 minutes

### Instruction
Test your core control at the edges of valid use. Movement needs held input and boundary handling; a menu choice needs one activation; planting needs enough resources; jumping needs a grounded condition. Write four cases: ordinary action, held action, rapid repeat and unavailable action. Choose the expected result before running. Fix the handler or guard rather than masking the issue with a visual effect. Preserve alternate controls and focus behavior while changing input code.

### Worked example
A repeat-input test might press a one-time choice rapidly. The accepted event happens once, while later calls observe the changed state and do nothing.

Scaffold C-PLATFORM — A gravity-and-jump platform player
Setup: CharacterBody2D root Player, child CollisionShape2D RectangleShape2D size (24,32), no scale changes. Layer 1 mask 2. StaticBody2D floor layer 2 mask 1 with rectangular collider. Add Input Map move_left, move_right and jump bound to Space. Place player above floor, not intersecting it. Coordinates use positive y downward. Attach script to Player.
```gdscript
extends CharacterBody2D
@export var speed: float = 160.0
@export var gravity: float = 900.0
@export var jump_speed: float = 340.0
func _draw() -> void:
    draw_rect(Rect2(-12, -16, 24, 32), Color.SKY_BLUE)
    draw_line(Vector2(-8, -4), Vector2(8, -4), Color.BLACK, 2.0)
func _physics_process(delta: float) -> void:
    velocity.x = Input.get_axis("move_left", "move_right") * speed
    if not is_on_floor():
        velocity.y += gravity * delta
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = -jump_speed
    move_and_slide()
```

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

### Assignment
1. Run four input-edge cases appropriate to your genre.
2. Repair any duplicate or invalid-action behavior.

### Acceptance checks
- Rapid repeat cannot duplicate a one-time reward.
- Unavailable actions leave protected state unchanged.

**Submit:** Input-hardening checkpoint and four-case table.

**Question:** Why can a held action and a just-pressed action need different handlers?

**Support:** Work on one task-board item at a time and checkpoint after each passing test. Do not expand the room count yet. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a harmless alternate input or optional route after required mechanics pass. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.

**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)

---

## VGC-L152 — Complete the central rule set

**Objective:** Implement every required rule without adding optional mechanics.

**Time:** 55 minutes

### Instruction
List the minimum rules from your contract and compare them with actual code. Finish the missing one with the highest dependency impact. For a multi-resource rule, validate all costs before mutation; for collection, enforce once-only rewards; for narrative, validate target IDs; for an arcade game, gate events by phase. Add a pure helper where the decision can be tested independently. Keep the rule’s authoritative data in the owner named by your architecture.

### Worked example
A repeat-input test might press a one-time choice rapidly. The accepted event happens once, while later calls observe the changed state and do nothing.

Scaffold C-PUZZLE — A testable gate rule
Setup: Create Node root GateLab and attach this script. The pure gate_open function does not read input or change a scene. A level can call it with its inventory and lever state. Assertions run on start. For the assignment write additional tests before changing the rule.
```gdscript
extends Node
func gate_open(has_key: bool, lever_on: bool, alarm_on: bool) -> bool:
    return has_key and lever_on and not alarm_on
func _ready() -> void:
    assert(gate_open(true, true, false))
    assert(not gate_open(false, true, false))
    assert(not gate_open(true, false, false))
    assert(not gate_open(true, true, true))
    print("Gate checks passed")
```

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-TESTS — A tiny pure-function test harness
Setup: Attach to Node TestLab. If using command line, create a separate SceneTree harness rather than running this Node script with --script. Run current TestLab scene in editor; assert reports a failed condition in Debugger. No external test framework or package is needed.
```gdscript
extends Node
func bounded_score(old_score: int, change: int) -> int:
    return clampi(old_score + change, 0, 999)
func _ready() -> void:
    assert(bounded_score(10, 2) == 12)
    assert(bounded_score(0, -3) == 0)
    assert(bounded_score(998, 9) == 999)
    print("3 score checks passed")
```

### Assignment
1. Finish the central rule and add three focused tests.
2. Update the rule-to-code map with actual function names.

### Acceptance checks
- Each required rule has a code location and test.
- No rule is enforced only by label text or visual button state.

**Submit:** Rule-complete checkpoint and rule/code/test map.

**Question:** What is the risk of enforcing a rule only in the interface?

**Support:** Work on one task-board item at a time and checkpoint after each passing test. Do not expand the room count yet. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a harmless alternate input or optional route after required mechanics pass. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.

**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)

---

## VGC-L153 — Complete a fair challenge and recovery path

**Objective:** Ensure failure teaches or resets rather than trapping the player.

**Time:** 55 minutes

### Instruction
Implement the challenge’s full response and recovery. A fall returns to a safe checkpoint, damage honors recovery time, a failed puzzle gives useful feedback, or an invalid transaction preserves resources. Test the worst legal action order you can imagine. If the game intentionally allows loss, provide a clear ending and restart instead of an unexplained soft lock. A soft lock is a state where play continues but the goal has become impossible without the player knowing why.

### Worked example
A repeat-input test might press a one-time choice rapidly. The accepted event happens once, while later calls observe the changed state and do nothing.

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-STATES — Explicit game states and gated scoring
Setup: Attach to Node root StateLab with child Label Status. This is a standalone demonstrator; call start_round, add_points, finish_round from buttons or Remote Inspector. In a real game the manager owns this state. Additional Player and spawner scripts must also honor the manager’s state, or be disabled when finishing.
```gdscript
extends Node
enum Phase { TITLE, PLAY, WIN, LOSE }
var phase: Phase = Phase.TITLE
var score: int = 0
func _ready() -> void:
    _show()
func start_round() -> void:
    score = 0
    phase = Phase.PLAY
    _show()
func add_points(amount: int) -> void:
    if phase != Phase.PLAY:
        return
    score += maxi(amount, 0)
    _show()
func finish_round(won: bool) -> void:
    if phase != Phase.PLAY:
        return
    phase = Phase.WIN if won else Phase.LOSE
    _show()
func _show() -> void:
    $Status.text = "State: %s | Score: %d" % [Phase.keys()[phase], score]
```

### Assignment
1. Implement recovery or explicit loss for the central challenge.
2. Test three failure sequences including a repeated event.

### Acceptance checks
- Every tested failure has a clear next action.
- No unexplained unwinnable state remains.

**Submit:** Challenge/recovery checkpoint and failure-sequence log.

**Question:** How is an explicit Lose screen different from a soft lock?

**Support:** Work on one task-board item at a time and checkpoint after each passing test. Do not expand the room count yet. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a harmless alternate input or optional route after required mechanics pass. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.

**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)

---

## VGC-L154 — Complete progress and end-state feedback

**Objective:** Keep the player’s view consistent with authoritative state.

**Time:** 55 minutes

### Instruction
Implement every required progress display and both the active and ended views. Test start, middle and completion values. A progress bar, number or story flag should derive from the manager or data owner. Make completion unmistakable using text and a non-color cue. Stop or gate obsolete gameplay after ending so score, damage and timers cannot continue changing. If there is only a win state, explain how the player can restart or leave without waiting.

### Worked example
A repeat-input test might press a one-time choice rapidly. The accepted event happens once, while later calls observe the changed state and do nothing.

Scaffold C-STATES — Explicit game states and gated scoring
Setup: Attach to Node root StateLab with child Label Status. This is a standalone demonstrator; call start_round, add_points, finish_round from buttons or Remote Inspector. In a real game the manager owns this state. Additional Player and spawner scripts must also honor the manager’s state, or be disabled when finishing.
```gdscript
extends Node
enum Phase { TITLE, PLAY, WIN, LOSE }
var phase: Phase = Phase.TITLE
var score: int = 0
func _ready() -> void:
    _show()
func start_round() -> void:
    score = 0
    phase = Phase.PLAY
    _show()
func add_points(amount: int) -> void:
    if phase != Phase.PLAY:
        return
    score += maxi(amount, 0)
    _show()
func finish_round(won: bool) -> void:
    if phase != Phase.PLAY:
        return
    phase = Phase.WIN if won else Phase.LOSE
    _show()
func _show() -> void:
    $Status.text = "State: %s | Score: %d" % [Phase.keys()[phase], score]
```

Scaffold C-MANAGER — A complete three-coin collection round
Setup: Create World Node2D root with Player (C-BODY), three Coin instances (C-PICKUP), and CanvasLayer HUD > Label named Score plus Button named Restart (text Restart, position 500,10). Root and parents unscaled. Name coins Coin1, Coin2, Coin3. Place them at (250,100),(380,220),(520,100). Player group player. Attach script to World. UI paths and coin names must match exactly. Connect nothing else in the editor. Label at (10,10).
```gdscript
extends Node2D
var score: int = 0
var playing: bool = true
@onready var score_label: Label = $HUD/Score
func _ready() -> void:
    $HUD/Restart.pressed.connect(_restart)
    for coin in [$Coin1, $Coin2, $Coin3]:
        coin.collected.connect(_on_collected)
    _refresh()
func _on_collected(points: int) -> void:
    if not playing:
        return
    score += points
    if score >= 3:
        playing = false
        $Player.set_physics_process(false)
    _refresh()
func _refresh() -> void:
    score_label.text = "Score: %d / 3" % score
    if not playing:
        score_label.text += " — Orchard complete!"
func _restart() -> void:
    get_tree().reload_current_scene()
```

### Assignment
1. Finish progress, ending text and post-ending event guards.
2. Compare internal and visible state at three milestones.

### Acceptance checks
- HUD matches data at all milestones.
- Events after ending cannot change the completed result.

**Submit:** Feedback-complete checkpoint and milestone comparison.

**Question:** Why stop a timer or gate its callback after completion?

**Support:** Work on one task-board item at a time and checkpoint after each passing test. Do not expand the room count yet. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a harmless alternate input or optional route after required mechanics pass. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.

**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)

---

## VGC-L155 — Run the core production regression

**Objective:** Prove the minimum loop before deeper-system work.

**Time:** 55 minutes

### Instruction
Freeze the core for one session and run launch, input, rule, challenge, progress, ending and replay tests from a restored artifact. Record pass/fail, actual behavior and issue ID. Fix the most serious reproducible defect, then rerun its test and adjacent tests. Do not add art or a second room while a required loop is broken. A clean core checkpoint is a recovery point for the next phase, where the richer system and content add complexity.

### Worked example
A repeat-input test might press a one-time choice rapidly. The accepted event happens once, while later calls observe the changed state and do nothing.

Scaffold C-TESTS — A tiny pure-function test harness
Setup: Attach to Node TestLab. If using command line, create a separate SceneTree harness rather than running this Node script with --script. Run current TestLab scene in editor; assert reports a failed condition in Debugger. No external test framework or package is needed.
```gdscript
extends Node
func bounded_score(old_score: int, change: int) -> int:
    return clampi(old_score + change, 0, 999)
func _ready() -> void:
    assert(bounded_score(10, 2) == 12)
    assert(bounded_score(0, -3) == 0)
    assert(bounded_score(998, 9) == 999)
    print("3 score checks passed")
```

### Assignment
1. Run the seven core acceptance tests from restored source.
2. Repair one failing issue or demonstrate a controlled fault repair in a duplicate.

### Acceptance checks
- Every pass has an actual result.
- The repaired case and neighboring features are retested.

**Submit:** Core-production source checkpoint and regression report.

**Question:** Why test from a restored artifact rather than only the open editor project?

**Support:** Work on one task-board item at a time and checkpoint after each passing test. Do not expand the room count yet. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add a harmless alternate input or optional route after required mechanics pass. Document one changed parameter, your prediction, and the observed effect; optional, not required for completion.

**Optional free reading:** [Your first 2D game](https://docs.godotengine.org/en/stable/getting_started/first_2d_game/index.html)

---