Video Game Design and Coding

Week 8 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 08 — Round state and fairness

## VGC-L036 — Track lives with a minimum

**Objective:** Maintain a numeric health rule and a visible explanation.

**Time:** 55 minutes

### Instruction
Lives are manager-owned integer state. Begin at three, subtract one for a valid hazard event, then clamp at zero. Only the Play state accepts damage. Display Lives: n in the HUD; a cross-marked hazard supplies another non-color cue. Guarding the pickup event prevents one object from counting twice, but separate hazards could still collide nearly together. Record that as a design issue rather than silently adding random delays. Later invulnerability can address it with a deliberate timing rule.

### Worked example
If phase is TITLE, a scoring event returns immediately. After start_round sets phase PLAY, the same event may add points. One exclusive phase prevents simultaneous WIN and LOSE flags.

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-HAZARD — Falling hazard with player notification and cleanup
Setup: Area2D Hazard root on layer 4 (bit 8), mask layer 1 only, child CollisionShape2D circle radius 12. Attach this script. Player in group player. A Player damage method is deliberately not hardcoded: connect hit_player to a manager’s damage handler when instantiating. Parent Hazards must be unscaled and unrotated.
```gdscript
extends Area2D
signal hit_player
@export var fall_speed: float = 110.0
var hit: bool = false
func _ready() -> void:
    body_entered.connect(_on_body_entered)
func _draw() -> void:
    draw_rect(Rect2(-12, -12, 24, 24), Color.TOMATO)
    draw_line(Vector2(-8,-8),Vector2(8,8),Color.BLACK,2.0)
func _physics_process(delta: float) -> void:
    position.y += fall_speed * delta
    if position.y > 390.0:
        queue_free()
func _on_body_entered(body: Node2D) -> void:
    if hit or not body.is_in_group("player"):
        return
    hit = true
    hit_player.emit()
    queue_free()
```

### Assignment
1. Add lives=3 and a damage handler with Play-state guard and clampi.
2. Test one hit, three hits and an extra hit after zero.

### Acceptance checks
- Lives follow 3,2,1,0 and never become negative.
- Damage outside Play leaves lives unchanged.

**Submit:** Lives implementation and boundary-test log.

**Question:** Is changing the Label text to zero enough to end the game?

**Support:** Print lives and phase in one Label before adding effects. Restrict every gameplay handler to the Play state. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Test simultaneous end conditions and write a deliberate tie policy. 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-L037 — Model explicit phases

**Objective:** Use an enum to prevent contradictory behavior.

**Time:** 55 minutes

### Instruction
An enum names a finite set of states. C-STATES demonstrates Title, Play, Win and Lose. In the real game, one manager owns phase and exposes functions for transitions. Start initializes score, lives and time before enabling player and spawner. End changes phase once, disables movement and stops spawning. A Boolean called playing can handle two states, but an enum makes several screens easier to distinguish. Keep state names independent from the wording shown in a label.

### Worked example
If phase is TITLE, a scoring event returns immediately. After start_round sets phase PLAY, the same event may add points. One exclusive phase prevents simultaneous WIN and LOSE flags.

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. Adapt C-STATES into Meteor Mail’s manager.
2. Write an event/state table and test scoring on Title, Play and Lose.

### Acceptance checks
- Only Play accepts scoring and damage.
- Win and Lose cannot both be active.

**Submit:** Four-state manager and state-event test table.

**Question:** Why is one phase variable safer than separate won and lost flags?

**Support:** Print lives and phase in one Label before adding effects. Restrict every gameplay handler to the Play state. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Test simultaneous end conditions and write a deliberate tie policy. 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-L038 — End a timed round deliberately

**Objective:** Use one clock and define the order of simultaneous outcomes.

**Time:** 55 minutes

### Instruction
A round clock should advance only during Play. You can use a one-shot RoundTimer with wait_time thirty or decrement a remaining-seconds variable in a frame function. Do not use both as competing authorities. Choose and document a tie rule: in this course, reaching zero lives takes precedence if a damage event is processed before the timeout transition; after a transition, further events are ignored. Start with a three-second test duration, then restore thirty seconds so tests are quick without changing the final rule.

### Worked example
If phase is TITLE, a scoring event returns immediately. After start_round sets phase PLAY, the same event may add points. One exclusive phase prevents simultaneous WIN and LOSE flags.

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. Add one authoritative round clock and timeout transition.
2. Test survival, zero lives first, and damage after an end state.

### Acceptance checks
- Survival reaches Win at timeout.
- A completed transition cannot be overwritten by a later event.

**Submit:** Timed game project and three end-condition tests.

**Question:** Why must timeout and damage handlers check the current phase?

**Support:** Print lives and phase in one Label before adding effects. Restrict every gameplay handler to the Play state. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Test simultaneous end conditions and write a deliberate tie policy. 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-L039 — Restart every piece of round state

**Objective:** Reset timers, objects, input and HUD consistently.

**Time:** 55 minutes

### Instruction
Restart is a transaction that restores a known start, not only a visual screen change. For this small game reload_current_scene can recreate player, hazards, timers and variables. If using a Start button, the reconstructed scene should wait at Title until pressed. Make a restart checklist: phase, score, lives, clock, player position, hazards and UI focus. A hidden paused SceneTree can survive some operations, so unpause intentionally before a scene reload if you have introduced pausing.

### Worked example
If phase is TITLE, a scoring event returns immediately. After start_round sets phase PLAY, the same event may add points. One exclusive phase prevents simultaneous WIN and LOSE flags.

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-TIMER — Timer-based spawning with a hard cap
Setup: World Node2D root contains Node2D Hazards and Timer SpawnTimer: wait_time 1.2, autostart On, one_shot Off. Hazard scene root Area2D has a CircleShape2D radius 12 and C-HAZARD. Attach script to World and drag Hazard.tscn into hazard_scene Inspector field. Set viewport 640x360. Name Hazards group not needed. This script is a separate lab; merge its _ready into an existing manager rather than declaring a second _ready.
```gdscript
extends Node2D
@export var hazard_scene: PackedScene
@export var spawn_limit: int = 12
func _ready() -> void:
    $SpawnTimer.timeout.connect(_spawn)
func _spawn() -> void:
    if hazard_scene == null or $Hazards.get_child_count() >= spawn_limit:
        return
    var hazard := hazard_scene.instantiate() as Area2D
    $Hazards.add_child(hazard)
    hazard.position = Vector2(randf_range(20.0, 620.0), -20.0)
```

### Assignment
1. Implement Restart and write expected initial values for seven state items.
2. Lose, restart, survive, restart and compare both starts.

### Acceptance checks
- No old hazards or score remain.
- Clock and input behave the same after either ending.

**Submit:** Restart implementation and seven-item reset checklist.

**Question:** What symptom suggests the timer was not reset?

**Support:** Print lives and phase in one Label before adding effects. Restrict every gameplay handler to the Play state. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Test simultaneous end conditions and write a deliberate tie policy. 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-L040 — Create a playable Meteor Mail checkpoint

**Objective:** Integrate loop, feedback and an understandable controls screen.

**Time:** 55 minutes

### Instruction
Integration checks interactions among systems. A spawner can work alone while failing in the complete game because the phase is still Title or a signal is never connected. Build a short controls panel describing movement, hazard symbol, three lives and thirty-second survival. Have a player start, play and restart without editor interaction. Keep safe deliveries optional until the complete avoidance loop works. Record known defects honestly; a checklist is evidence, not a claim that every test must pass on the first attempt.

### Worked example
If phase is TITLE, a scoring event returns immediately. After start_round sets phase PLAY, the same event may add points. One exclusive phase prevents simultaneous WIN and LOSE flags.

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-TIMER — Timer-based spawning with a hard cap
Setup: World Node2D root contains Node2D Hazards and Timer SpawnTimer: wait_time 1.2, autostart On, one_shot Off. Hazard scene root Area2D has a CircleShape2D radius 12 and C-HAZARD. Attach script to World and drag Hazard.tscn into hazard_scene Inspector field. Set viewport 640x360. Name Hazards group not needed. This script is a separate lab; merge its _ready into an existing manager rather than declaring a second _ready.
```gdscript
extends Node2D
@export var hazard_scene: PackedScene
@export var spawn_limit: int = 12
func _ready() -> void:
    $SpawnTimer.timeout.connect(_spawn)
func _spawn() -> void:
    if hazard_scene == null or $Hazards.get_child_count() >= spawn_limit:
        return
    var hazard := hazard_scene.instantiate() as Area2D
    $Hazards.add_child(hazard)
    hazard.position = Vector2(randf_range(20.0, 620.0), -20.0)
```

Scaffold C-HAZARD — Falling hazard with player notification and cleanup
Setup: Area2D Hazard root on layer 4 (bit 8), mask layer 1 only, child CollisionShape2D circle radius 12. Attach this script. Player in group player. A Player damage method is deliberately not hardcoded: connect hit_player to a manager’s damage handler when instantiating. Parent Hazards must be unscaled and unrotated.
```gdscript
extends Area2D
signal hit_player
@export var fall_speed: float = 110.0
var hit: bool = false
func _ready() -> void:
    body_entered.connect(_on_body_entered)
func _draw() -> void:
    draw_rect(Rect2(-12, -12, 24, 24), Color.TOMATO)
    draw_line(Vector2(-8,-8),Vector2(8,8),Color.BLACK,2.0)
func _physics_process(delta: float) -> void:
    position.y += fall_speed * delta
    if position.y > 390.0:
        queue_free()
func _on_body_entered(body: Node2D) -> void:
    if hit or not body.is_in_group("player"):
        return
    hit = true
    hit_player.emit()
    queue_free()
```

### Assignment
1. Integrate Title, active round, both endings and Restart.
2. Run an end-to-end test from a fresh launch with no Remote Inspector changes.

### Acceptance checks
- The full loop can be completed using game controls only.
- Every known failed test has a reproduction note.

**Submit:** Meteor Mail checkpoint ZIP and lifecycle test log.

**Question:** Why can individually working systems fail when combined?

**Support:** Print lives and phase in one Label before adding effects. Restrict every gameplay handler to the Play state. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.

**Optional extension:** Test simultaneous end conditions and write a deliberate tie policy. 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)

---