Video Game Design and Coding

Week 17 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 17 — Midyear production

## VGC-L081 — Complete the core mechanic

**Objective:** Make the main player action reliable across ordinary cases.

**Time:** 55 minutes

### Instruction
Choose the one action that defines your game: collect, dodge, choose dialogue or jump. Test it from at least three start positions or states. Replace any editor-only shortcut with a real game control. If using a copied script from your earlier work, explain which lines are reused and which change the new rule. A mechanic is not complete if it works only in the exact position used while developing. Save a new checkpoint before adding the next system.

### Worked example
An input-to-feedback chain might be movement, pickup overlap, collected signal, manager score increment, then label refresh. Each stage has one clear responsibility.

Scaffold C-BODY — Reusable top-down player with collision
Setup: Create CharacterBody2D root Player at (80,180); add CollisionShape2D with CircleShape2D radius 14. Attach this script to Player and save Player.tscn. Use C-FREE-MOVE input actions. Root collision layer 1, mask 2. Create a separate StaticBody2D Wall on layer 2 mask 1 with RectangleShape2D collision and a Polygon2D rectangle as visible child. Instance both under an unscaled Node2D World. Do not put a body inside another physics body.
```gdscript
extends CharacterBody2D
@export var speed: float = 160.0
func _draw() -> void:
    draw_circle(Vector2.ZERO, 14.0, Color.CORNFLOWER_BLUE)
    draw_line(Vector2(-8, 0), Vector2(8, 0), Color.WHITE, 3.0)
func _physics_process(_delta: float) -> void:
    velocity = Input.get_vector("move_left", "move_right", "move_up", "move_down") * speed
    move_and_slide()
```

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

### Assignment
1. Finish the core action and test three ordinary cases.
2. Annotate reused versus newly written code.

### Acceptance checks
- All three cases produce the intended state change.
- Gameplay does not require Remote Inspector edits.

**Submit:** Core-mechanic checkpoint and three-case log.

**Question:** Why identify reused code rather than claiming everything is new?

**Support:** Use your state-owner diagram to fix one system at a time. Keep the vertical-slice checkpoint available for comparison. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add one optional challenge only after the complete minimum loop passes. 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-L082 — Implement the central challenge fairly

**Objective:** Connect obstacle behavior to a predictable game rule.

**Time:** 55 minutes

### Instruction
Build the challenge promised in your proposal: hazard damage, a lock condition, a route obstacle or a resource limit. Define what happens on failure and how the player can recover or restart. Use phase guards and exactly-once flags where needed. A challenge should be understandable from consistent cues. Test an ordinary success, an ordinary failure and a repeated event. Do not add random punishment simply to make the game seem longer.

### Worked example
An input-to-feedback chain might be movement, pickup overlap, collected signal, manager score increment, then label refresh. Each stage has one clear responsibility.

Scaffold C-PICKUP — An exactly-once pickup signal
Setup: Create Area2D root Coin, CollisionShape2D Circle radius 10. Layer 3 (bit value 4), mask layer 1 only. Monitoring on. Attach script and save Coin.tscn. In the Node > Groups editor add Player root to group player. Put a Coin instance and Player instance in World. Manager connects each Coin.collected to its score handler (see C-MANAGER). Signal wiring here is code-only: do not also connect body_entered in editor.
```gdscript
extends Area2D
signal collected(points: int)
@export var points: int = 1
var taken: bool = false
func _ready() -> void:
    body_entered.connect(_on_body_entered)
func _draw() -> void:
    draw_circle(Vector2.ZERO, 10.0, Color.GOLD)
    draw_line(Vector2(-5, 0), Vector2(5, 0), Color.BLACK, 2.0)
func _on_body_entered(body: Node2D) -> void:
    if taken or not body.is_in_group("player"):
        return
    taken = true
    collected.emit(points)
    queue_free()
```

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-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 the central challenge and its feedback.
2. Run success, failure and repeated-event tests.

### Acceptance checks
- The rule is applied consistently.
- Repeated events do not duplicate rewards or produce negative resources.

**Submit:** Challenge checkpoint and three-event test evidence.

**Question:** Why test a repeated event separately?

**Support:** Use your state-owner diagram to fix one system at a time. Keep the vertical-slice checkpoint available for comparison. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add one optional challenge only after the complete minimum loop passes. 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-L083 — Make state visible and controls discoverable

**Objective:** Connect HUD and instructions to the actual game rules.

**Time:** 55 minutes

### Instruction
A player should know what they can do, what the goal is and whether an action changed the game. Add a concise control panel and progress feedback derived from authoritative state. Test text on the busiest background and show critical information without depending on sound or color alone. The instructions must match current bindings; stale instructions from an earlier project are a real defect. Do not claim support for controller, touch or screen readers unless you actually implemented and tested it.

### Worked example
An input-to-feedback chain might be movement, pickup overlap, collected signal, manager score increment, then label refresh. Each stage has one clear responsibility.

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

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

### Assignment
1. Add goal, controls, progress and ending feedback.
2. Compare each instruction with a real input test.

### Acceptance checks
- All stated controls work.
- Progress display matches internal state at start, middle and end.

**Submit:** Feedback checkpoint and instruction-verification table.

**Question:** Why can stale instructions make a technically working mechanic unusable?

**Support:** Use your state-owner diagram to fix one system at a time. Keep the vertical-slice checkpoint available for comparison. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add one optional challenge only after the complete minimum loop passes. 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-L084 — Make a complete replay loop

**Objective:** Verify every ending returns to a clean playable start.

**Time:** 55 minutes

### Instruction
Finish Restart or New Round with the reset policy in your proposal. List every state item that must reset, including hidden flags, cooldowns, clock, inventory and checkpoint. If you use scene reload, confirm pause is cleared before reloading and that any global state has a deliberate policy. Play two full rounds with different outcomes if the game has both win and loss. A replay test catches state leaks that a fresh editor run hides.

### Worked example
An input-to-feedback chain might be movement, pickup overlap, collected signal, manager score increment, then label refresh. Each stage has one clear responsibility.

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-PAUSE — Pause with an always-processing resume control
Setup: Node2D World root has CanvasLayer HUD with Button Pause, plus normal gameplay children. Set Pause button Process > Mode to Always in Inspector; leave World and gameplay as Inherit/Pausable. Attach script to World; merge into its existing _ready if present. Signal callbacks still work from the always-processing button. Label text starts Pause.
```gdscript
extends Node2D
func _ready() -> void:
    $HUD/Pause.pressed.connect(_toggle_pause)
func _toggle_pause() -> void:
    get_tree().paused = not get_tree().paused
    $HUD/Pause.text = "Resume" if get_tree().paused else "Pause"
```

### Assignment
1. Implement replay and run two consecutive rounds.
2. Compare all reset-state items at each start.

### Acceptance checks
- Second-round behavior matches the first-round rules.
- No stale object, disabled input or ended timer survives unintentionally.

**Submit:** Replay checkpoint and reset-state comparison.

**Question:** Why can restarting the editor hide a reset bug?

**Support:** Use your state-owner diagram to fix one system at a time. Keep the vertical-slice checkpoint available for comparison. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add one optional challenge only after the complete minimum loop passes. 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-L085 — Freeze features and build a test candidate

**Objective:** Create a reproducible version for outside feedback.

**Time:** 55 minutes

### Instruction
A feature freeze means required features are in place and new ideas go to a later list. Name a test-candidate version such as midyear-rc1 and create a restore-tested ZIP. Include controls, goal, known issues and the eight acceptance tests. Do not edit that exact candidate while someone is testing it; make the next revision separately. This keeps a bug report tied to a reproducible version. A candidate may still contain bugs, which is why it exists.

### Worked example
An input-to-feedback chain might be movement, pickup overlap, collected signal, manager score increment, then label refresh. Each stage has one clear responsibility.

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. Package midyear-rc1 with readme and test contract.
2. Restore and run it in a separate folder before testing.

### Acceptance checks
- Artifact version is written in the readme and test sheet.
- Restored candidate reproduces the same behavior.

**Submit:** Midyear release-candidate source artifact and test sheet.

**Question:** Why avoid editing the tested candidate in place?

**Support:** Use your state-owner diagram to fix one system at a time. Keep the vertical-slice checkpoint available for comparison. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Add one optional challenge only after the complete minimum loop passes. 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)

---