Week 30 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 30 — Final vertical slice
## VGC-L146 — Create the final project baseline
**Objective:** Build a clean, versioned project without stale dependencies.
**Time:** 55 minutes
### Instruction
Create a new local Godot project or a carefully cleaned copy of your own prior work. Set the course viewport and renderer, configure input actions and create the planned folders. Remove unused scenes and imported assets that are not licensed or needed. Add a readme with working title, controls and engine version. Save a baseline snapshot that launches the intended main scene. Do not begin with a large downloaded template you cannot explain.
### Worked example
A vertical slice joins a player action, one challenge, progress feedback and an ending. It demonstrates real integration even while the art remains rectangles.
Scaffold C-HELLO — An observable first script
Setup: Create a Node scene, name root Lab, attach lab.gd. Save as lab.tscn. Run current scene (editor clapboard/Run Current Scene). Output panel shows print output. All unindented lines belong to the script and indented lines belong to the function.
```gdscript
extends Node
var deliveries: int = 0
func _ready() -> void:
deliveries += 1
print("Deliveries: ", deliveries)
```
### Assignment
1. Create baseline project with named main scene, folders and input map.
2. Restore baseline ZIP and launch it.
### Acceptance checks
- The correct scene opens without missing resources.
- Readme names Godot 4.7.2 and actual controls.
**Submit:** Final baseline source artifact and setup readme.
**Question:** Why remove stale copied scenes and assets?
**Support:** Use placeholder shapes and one room. Keep all twelve acceptance tests visible, with honest not-yet-implemented statuses. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Add one optional visual improvement only after the slice is playable end to end. 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-L147 — Implement the final player action
**Objective:** Make the core input-to-action path observable.
**Time:** 55 minutes
### Instruction
Implement the action central to the final game using your architecture. For movement, verify both main and alternate keys and the collision footprint. For narrative, verify choice activation and focus. For a resource game, verify a request reaches validation before changing quantities. Use temporary diagnostics to show the event path, then keep player-facing feedback clear. Test three start positions or states so the mechanic is not accidentally tied to a single development setup.
### Worked example
A vertical slice joins a player action, one challenge, progress feedback and an ending. It demonstrates real integration even while the art remains rectangles.
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()
```
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. Build the core action and three ordinary-case tests.
2. Trace one input to its state effect in a short note.
### Acceptance checks
- All three cases produce expected behavior.
- The input path matches the architecture diagram.
**Submit:** Final core-action checkpoint and test note.
**Question:** Why test more than the initial spawn state?
**Support:** Use placeholder shapes and one room. Keep all twelve acceptance tests visible, with honest not-yet-implemented statuses. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Add one optional visual improvement only after the slice is playable end to end. 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-L148 — Prototype the richer system in isolation
**Objective:** Validate the advanced component before full integration.
**Time:** 55 minutes
### Instruction
Build the chosen save, AI or generation system in a small test scene within the final project. Saves need missing/malformed-data tests; AI needs state-boundary and missing-target tests; generation needs seed and reserved-route tests. Use the course recipe as a scaffold and make the game-specific changes yourself. Keep test scenes clearly named so they are not confused with the main scene. This stage reduces debugging ambiguity when the component joins the playable loop.
### Worked example
A vertical slice joins a player action, one challenge, progress feedback and an ending. It demonstrates real integration even while the art remains rectangles.
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-GUARD-AI — A readable patrol/chase/return state machine
Setup: Create CharacterBody2D Guard with visible circle or polygon and matching collider. Layer 4 mask 2 for walls. Attach script and assign target to your Player instance in Inspector. Level is one open rectangle; this is steering, not navigation around walls. Damage remains a separate sensor/manager event. Draw detection radii on a design diagram before tuning values.
```gdscript
extends CharacterBody2D
enum Mode { PATROL, CHASE, RETURN }
@export var target: Node2D
@export var speed: float = 70.0
var mode: Mode = Mode.PATROL
var home: Vector2
var patrol_target: Vector2
func _ready() -> void:
home = global_position
patrol_target = home + Vector2(80, 0)
func _physics_process(delta: float) -> void:
if not is_instance_valid(target):
velocity = Vector2.ZERO
return
var distance: float = global_position.distance_to(target.global_position)
if mode != Mode.CHASE and distance < 120.0:
mode = Mode.CHASE
elif mode == Mode.CHASE and distance > 180.0:
mode = Mode.RETURN
var destination: Vector2
if mode == Mode.CHASE:
destination = target.global_position
elif mode == Mode.RETURN:
destination = home
if global_position.distance_to(home) <= maxf(3.0, speed * delta):
mode = Mode.PATROL
else:
destination = patrol_target
if global_position.distance_to(patrol_target) <= maxf(3.0, speed * delta):
patrol_target = home if patrol_target != home else home + Vector2(80, 0)
var offset := destination - global_position
velocity = offset.normalized() * minf(speed, offset.length() / maxf(delta, 0.001))
move_and_slide()
```
Scaffold C-GRID — A deterministic grid with a guaranteed clear route
Setup: Attach to Node2D GridLab. This example draws, not physics, so the assignment adds matching wall collision only after validation. Grid is 8 columns by 6 rows at 40 pixels per cell. Row 0 and column 7 are a guaranteed clear route from (0,0) to (7,5). RNG seed is a nonpersonal integer.
```gdscript
extends Node2D
const COLS: int = 8
const ROWS: int = 6
const CELL: int = 40
var cells: Array = []
func _ready() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = 2468
for y in range(ROWS):
var row: Array[int] = []
for x in range(COLS):
var blocked: bool = y != 0 and x != COLS - 1 and rng.randf() < 0.25
row.append(1 if blocked else 0)
cells.append(row)
queue_redraw()
func _draw() -> void:
for y in range(cells.size()):
for x in range(cells[y].size()):
var color := Color.DIM_GRAY if cells[y][x] == 1 else Color.BEIGE
draw_rect(Rect2(x * CELL, y * CELL, CELL - 2, CELL - 2), color)
```
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. Create a focused test scene for the richer system.
2. Run one ordinary, one boundary and one invalid/missing-input case.
### Acceptance checks
- All three results match documented rules.
- The component does not require a public service or account.
**Submit:** Richer-system lab and three-case test log.
**Question:** Why keep a component test scene after integration?
**Support:** Use placeholder shapes and one room. Keep all twelve acceptance tests visible, with honest not-yet-implemented statuses. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Add one optional visual improvement only after the slice is playable end to end. 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-L149 — Integrate the first final-game slice
**Objective:** Connect input, challenge, progress and an ending.
**Time:** 55 minutes
### Instruction
Join one instance of each required system into a thin playable route. The richer component must affect actual play, not sit unused in a hidden lab. A save should restore meaningful progress, an AI should influence a decision, or generation should produce the playable room. Use placeholders and one short challenge. Add a provisional ending and restart. Record which of the twelve acceptance tests now pass and which remain unimplemented; do not disguise a partial slice as the final course project.
### Worked example
A vertical slice joins a player action, one challenge, progress feedback and an ending. It demonstrates real integration even while the art remains rectangles.
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-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. Build one complete route and connect the richer system.
2. Run the first slice against all twelve tests with honest statuses.
### Acceptance checks
- The slice is playable from launch to ending.
- The richer system has an observable role in the loop.
**Submit:** Final vertical-slice source and twelve-test status table.
**Question:** What makes an advanced system genuinely integrated?
**Support:** Use placeholder shapes and one room. Keep all twelve acceptance tests visible, with honest not-yet-implemented statuses. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Add one optional visual improvement only after the slice is playable end to end. 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-L150 — Review the slice and lock the minimum scope
**Objective:** Make a documented decision about what remains feasible.
**Time:** 55 minutes
### Instruction
Review the vertical slice with a parent or use the rubric yourself. Demonstrate the loop and show the test table rather than only discussing planned features. Identify the biggest gap, the riskiest dependency and one feature to defer. Update the task board. Lock the minimum scope for the production phase, allowing changes only when documented for feasibility, safety or correctness. Optional content can grow later, but a complete reliable core takes priority.
### Worked example
A vertical slice joins a player action, one challenge, progress feedback and an ending. It demonstrates real integration even while the art remains rectangles.
### Assignment
1. Write a slice-review summary with three decisions.
2. Update dependencies and minimum scope on the task board.
### Acceptance checks
- Decisions cite current evidence.
- The required game still includes a complete loop and one tested richer system.
**Submit:** Slice review and updated production plan.
**Question:** Why lock a minimum rather than forbid every later change?
**Support:** Use placeholder shapes and one room. Keep all twelve acceptance tests visible, with honest not-yet-implemented statuses. Start with the first assigned task and re-run its acceptance test before adding the remaining feature.
**Optional extension:** Add one optional visual improvement only after the slice is playable end to end. 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)
---
