Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .serena/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,17 @@ read_only_memory_patterns: []
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []

# list of APIs (facades or facade methods, e.g. "lsp" or "lsp.get_diagnostics_for_symbol") to include in the REPL
# that would otherwise be disabled (particularly optional methods, which are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_apis: []

# list of APIs (facades or facade methods, e.g. "lsp" or "lsp.find_symbol") to exclude from the REPL.
# This extends the existing exclusions (e.g. from the global configuration).
excluded_apis: []

# The interface through which the agent (LLM) accesses Serena's functionality (overrides the global setting).
# Valid values: tools, REPL (see the global configuration for details); leave empty to use the global setting.
# Note: the interface is fixed at startup. If a project is activated post-init, its setting is not applied.
agent_interface:
5 changes: 5 additions & 0 deletions game/content/loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const TUNING_SCHEMA := {
"grid_line_alpha": "number",
"device_pixel_ratio_cap": "number",
"pause_vignette_alpha": "number",
"wall_border_width_px": "number",
"wall_border_alpha": "number",
"wall_border_wrap_alpha": "number",
"wall_border_wrap_dash_fraction": "number",
},
"input": {
"swipe_threshold_cell_fraction": "number",
Expand Down Expand Up @@ -82,6 +86,7 @@ const PALETTE_SCHEMA := {
"background": TYPE_STRING,
"checkerboard_tile": TYPE_STRING,
"grid_line": TYPE_STRING,
"wall_border": TYPE_STRING,
"food_fill": TYPE_STRING,
"food_shadow": TYPE_STRING,
"snake_head_fill": TYPE_STRING,
Expand Down
18 changes: 15 additions & 3 deletions game/content/palette.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,26 @@
"background": "#0c1628",
"checkerboard_tile": "#ffffff",
"grid_line": "#38bdf8",
"wall_border": "#e2e8f0",
"food_fill": "#ef4444",
"food_shadow": "#f87171",
"snake_head_fill": "#a7f3d0",
"snake_head_shadow": "#4ade80",
"snake_body_base_rgb": [74, 222, 128],
"snake_body_rgb_delta": [30, -20, 30],
"snake_body_base_rgb": [
74,
222,
128
],
"snake_body_rgb_delta": [
30,
-20,
30
],
"eye_fill": "#062c1a",
"particle_hues": ["#f87171", "#fbbf24"],
"particle_hues": [
"#f87171",
"#fbbf24"
],
"flash_dead": "#ef4444",
"flash_eat": "#fbbf24",
"pause_vignette": "#060b16"
Expand Down
6 changes: 5 additions & 1 deletion game/content/tuning.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
"checkerboard_alpha": 0.019,
"grid_line_alpha": 0.055,
"device_pixel_ratio_cap": 2,
"pause_vignette_alpha": 0.55
"pause_vignette_alpha": 0.55,
"wall_border_width_px": 2.5,
"wall_border_alpha": 0.3,
"wall_border_wrap_alpha": 0.1,
"wall_border_wrap_dash_fraction": 0.55
},
"input": {
"swipe_threshold_cell_fraction": 0.5
Expand Down
2 changes: 1 addition & 1 deletion game/main.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
layout_mode = 3
anchors_preset = 0
offset_right = 520.0
offset_bottom = 600.0
offset_bottom = 545.0
script = ExtResource("1")
32 changes: 30 additions & 2 deletions game/presentation/board/board_geometry.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ extends RefCounted
## operationalized as data so board_view.gd's _draw() order can be pinned by
## a test instead of relying on code review alone (AC#4).
const DRAW_LAYER_ORDER := [
"background", "checkerboard", "grid", "food", "snake", "eyes",
"background", "checkerboard", "grid", "walls", "food", "snake", "eyes",
"particles", "flash", "pause_vignette",
]

Expand Down Expand Up @@ -96,7 +96,7 @@ static func build_checkerboard_image(cols: int, rows: int, tile_color: Color, al

## Decodes the fields of docs/canonical-state.md's 44-byte header that
## board_view.gd needs and has no other accessor for (cols/rows/food
## position aren't exposed by SimulationWorld.player_view_get or
## position/wrap flag aren't exposed by SimulationWorld.player_view_get or
## .body_copy). PackedByteArray.decode_u16 is little-endian, matching the
## canonical format's own endianness.
static func decode_canon_header(bytes: PackedByteArray) -> Dictionary:
Expand All @@ -105,4 +105,32 @@ static func decode_canon_header(bytes: PackedByteArray) -> Dictionary:
"rows": bytes.decode_u16(12),
"food_x": bytes.decode_u16(40),
"food_y": bytes.decode_u16(42),
"wrap": (bytes.decode_u16(14) & 1) != 0,
}

## draw_rect()'s unfilled outline is centered on its own boundary, so a
## `thickness`-px border around the board must be inset by half its own
## width on every side to stay inside the 0..cols*cell / 0..rows*cell rect
## that core/world.zig:340's bounds check actually kills you on.
static func wall_border_rect(cols: int, rows: int, cell: float, thickness: float) -> Rect2:
var half := thickness / 2.0
return Rect2(half, half, cols * cell - thickness, rows * cell - thickness)

## One dash per perimeter cell (`wrap` mode's edge is passable, so the
## border reads as a broken line rather than solid), each centered on that
## cell along the same half-thickness-inset boundary wall_border_rect()
## draws solid. Returns an Array of [from: Vector2, to: Vector2] pairs.
static func wall_dash_segments(cols: int, rows: int, cell: float, thickness: float, dash_fraction: float) -> Array:
var half := thickness / 2.0
var dash := cell * dash_fraction
var pad := (cell - dash) / 2.0
var out := []
for x in cols:
var cx := x * cell + pad
out.append([Vector2(cx, half), Vector2(cx + dash, half)])
out.append([Vector2(cx, rows * cell - half), Vector2(cx + dash, rows * cell - half)])
for y in rows:
var cy := y * cell + pad
out.append([Vector2(half, cy), Vector2(half, cy + dash)])
out.append([Vector2(cols * cell - half, cy), Vector2(cols * cell - half, cy + dash)])
return out
16 changes: 16 additions & 0 deletions game/presentation/board/board_view.gd
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func _draw() -> void:
_draw_background(cols, rows, cell)
_draw_checkerboard(cols, rows, cell)
_draw_grid(cols, rows, cell)
_draw_walls(header, cols, rows, cell)
_draw_food(header, cell)
if body.result == SimulationWorld.OK:
_draw_snake(body.cells, cell)
Expand Down Expand Up @@ -103,6 +104,21 @@ func _draw_grid(cols: int, rows: int, cell: float) -> void:
for y in BoardGeometry.grid_line_offsets(rows, cell):
draw_line(Vector2(0, y), Vector2(cols * cell, y), color, 1.0)

## Solid in wall mode (the edge kills you); dim and dashed in wrap mode
## (the edge is passable) -- reference/snake.html has no equivalent, this
## is the port's own answer to grid_line_offsets() drawing interior lines
## only (board_geometry.gd:78-82) and leaving the actual kill line invisible.
func _draw_walls(header: Dictionary, cols: int, rows: int, cell: float) -> void:
var width: float = tuning.render.wall_border_width_px
var color := Color(palette.board.wall_border)
if header.wrap:
color.a = tuning.render.wall_border_wrap_alpha
for segment in BoardGeometry.wall_dash_segments(cols, rows, cell, width, tuning.render.wall_border_wrap_dash_fraction):
draw_line(segment[0], segment[1], color, width)
else:
color.a = tuning.render.wall_border_alpha
draw_rect(BoardGeometry.wall_border_rect(cols, rows, cell, width), color, false, width)

func _draw_food(header: Dictionary, cell: float) -> void:
if header.food_x == BoardGeometry.NO_CELL_COORD:
return
Expand Down
29 changes: 18 additions & 11 deletions game/presentation/screens/game_screen.gd
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ var _save_data: Dictionary
var _settings_open := false

func _ready() -> void:
## project.godot's viewport_width/height (520x600) is only the design
## project.godot's viewport_width/height (520x545) is only the design
## canvas that canvas_items/keep stretch scales up to fill the window --
## it's also the actual initial OS window size unless overridden here,
## since window_width_override/height_override (the project-settings
Expand All @@ -99,12 +99,12 @@ func _ready() -> void:
## QHD/4K display doesn't blow the board up past a sane on-screen size.
## Window.size is in physical pixels, not the OS window manager's points
## -- on a Retina/hiDPI display (screen_get_scale() > 1) an unscaled
## Vector2i(780, 900) here nets a window that visually measures half
## Vector2i(780, 817.5) here nets a window that visually measures half
## that on screen, so the target size must be scaled up to compensate.
var screen := DisplayServer.window_get_current_screen()
var display_scale := DisplayServer.screen_get_scale(screen)
get_window().size = Vector2i(Vector2(780, 900) * display_scale)
get_window().max_size = Vector2i(Vector2(936, 1080) * display_scale)
get_window().size = Vector2i(Vector2(780, 817.5) * display_scale)
get_window().max_size = Vector2i(Vector2(936, 981) * display_scale)

var content := ContentLoader.load_all()
if not content.ok:
Expand Down Expand Up @@ -169,6 +169,8 @@ func _ready() -> void:
overlay.return_to_title_requested.connect(_on_return_to_title_requested)
overlay.mode_selected.connect(_on_mode_selected)
overlay.settings_requested.connect(_on_settings_requested)
overlay.set_quit_available(not OS.has_feature("web"))
overlay.quit_requested.connect(_quit_game)

settings_panel = SettingsPanel.new()
settings_panel.position = board_view.position
Expand Down Expand Up @@ -235,16 +237,21 @@ func _capture_state_arg() -> String:
return arg.substr("--capture-state=".length())
return ""

## NOTIFICATION_WM_CLOSE_REQUEST fires for both a window close and a
## terminal SIGINT, so routing it through _quit_game() covers both.
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
_quit_game()

## A still-looping AudioStreamOggVorbis at process exit leaks its playback
## objects (AudioStreamPlaybackOggVorbis/OggPacketSequencePlayback) --
## Godot only releases them cleanly once AudioStreamPlayer.stop() has run,
## not merely on the node being freed. NOTIFICATION_WM_CLOSE_REQUEST fires
## for both a window close and a terminal SIGINT, so this covers both.
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
if music != null:
music.stop()
get_tree().quit()
## not merely on the node being freed. Shared by the window-close path
## above and the title screen's own Quit button.
func _quit_game() -> void:
if music != null:
music.stop()
get_tree().quit()

func _process(delta: float) -> void:
var pre_food_x := BoardGeometry.NO_CELL_COORD
Expand Down
4 changes: 4 additions & 0 deletions game/presentation/screens/game_screen_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ static func overlay_content(screen: String, score: int, best: int, snake_len: in
"show_mode_select": true,
"show_player_select": true,
"show_return_to_title": false,
"show_quit": true,
"controls": CONTROLS_LEGEND,
}
SCREEN_PAUSED:
Expand All @@ -61,6 +62,7 @@ static func overlay_content(screen: String, score: int, best: int, snake_len: in
"show_mode_select": false,
"show_player_select": false,
"show_return_to_title": false,
"show_quit": false,
"controls": "",
}
SCREEN_DEAD:
Expand All @@ -72,6 +74,7 @@ static func overlay_content(screen: String, score: int, best: int, snake_len: in
"show_mode_select": false,
"show_player_select": false,
"show_return_to_title": true,
"show_quit": false,
"controls": "",
}
return {
Expand All @@ -81,6 +84,7 @@ static func overlay_content(screen: String, score: int, best: int, snake_len: in
"show_mode_select": false,
"show_player_select": false,
"show_return_to_title": true,
"show_quit": false,
"controls": "",
}
_:
Expand Down
33 changes: 19 additions & 14 deletions game/presentation/screens/hud.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,44 @@ extends Control
## (snake.html:218-223), which this task's text never asks for. Built in
## code, matching board_view.gd's code-first Control convention: this repo
## has no .tscn precedent to follow instead.
##
## Both players' fields share a single HBoxContainer row (game_screen.gd
## sets custom_minimum_size.y to the 25px strip below the board, which only
## fits one text row) rather than the two stacked rows an earlier version
## used.

var _score_label := Label.new()
var _best_label := Label.new()
var _status_label := Label.new()

## TASK-051: player 1's own row. No best-score label -- best-score
## TASK-051: player 1's own fields. No best-score label -- best-score
## persistence stays player-0/mode-scoped only (see backlog/decisions).
var _p2_score_label := Label.new()
var _p2_status_label := Label.new()
var _p2_box: HBoxContainer

const _FONT_SIZE := 14

func _ready() -> void:
## Both rows are plain Control children with no shared layout container,
## so without this VBoxContainer wrapper they both default to (0, 0)
## and render on top of each other.
var rows := VBoxContainer.new()
add_child(rows)

var box := HBoxContainer.new()
box.add_theme_constant_override("separation", 16)
rows.add_child(box)
box.add_child(_score_label)
box.add_child(_best_label)
box.add_child(_status_label)
custom_minimum_size = Vector2(0, 25)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 16)
add_child(row)
row.add_child(_score_label)
row.add_child(_best_label)
row.add_child(_status_label)
update(0, 0, "")

_p2_box = HBoxContainer.new()
_p2_box.add_theme_constant_override("separation", 16)
rows.add_child(_p2_box)
row.add_child(_p2_box)
_p2_box.add_child(_p2_score_label)
_p2_box.add_child(_p2_status_label)
update_p2(0, "")

for label in [_score_label, _best_label, _status_label, _p2_score_label, _p2_status_label]:
label.add_theme_font_size_override("font_size", _FONT_SIZE)

## status_text mirrors the S.status equivalent transition currently in
## effect (menu/playing/paused/dead) -- not part of the oracle's own HUD,
## but explicit in this task's Description ("status text matching S.status
Expand Down
19 changes: 19 additions & 0 deletions game/presentation/screens/overlay_panel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ signal player_count_selected(count: int)
signal return_to_title_requested
signal mode_selected(mode_id: String)
signal settings_requested
signal quit_requested

var _title_label := Label.new()
var _sub_label := Label.new()
Expand All @@ -42,6 +43,14 @@ var _action_button := Button.new()
var _return_button := Button.new()

var _settings_button := Button.new()

## Menu-only, same as the mode select/player-count buttons. Hidden outright
## on a platform where quitting is impossible (a browser tab -- see
## game_screen.gd's OS.has_feature("web") check) via set_quit_available(),
## not just left visible and inert.
var _quit_button := Button.new()
var _quit_available := true

var _mode_ids: Array[String] = []

## Tracks the visible->hidden/hidden->visible edge across configure() calls
Expand Down Expand Up @@ -73,12 +82,15 @@ func _ready() -> void:
box.add_child(_return_button)
_settings_button.text = "Settings"
box.add_child(_settings_button)
_quit_button.text = "Quit"
box.add_child(_quit_button)
_action_button.pressed.connect(func() -> void: action_pressed.emit())
_return_button.pressed.connect(func() -> void: return_to_title_requested.emit())
_one_player_button.pressed.connect(func() -> void: player_count_selected.emit(1))
_two_player_button.pressed.connect(func() -> void: player_count_selected.emit(2))
_mode_select.item_selected.connect(_on_item_selected)
_settings_button.pressed.connect(func() -> void: settings_requested.emit())
_quit_button.pressed.connect(func() -> void: quit_requested.emit())

func _on_item_selected(index: int) -> void:
mode_selected.emit(_mode_ids[index])
Expand Down Expand Up @@ -118,3 +130,10 @@ func configure(content: Dictionary) -> void:
_controls_label.text = content.controls
_controls_label.visible = content.controls != ""
_mode_select.visible = content.show_mode_select
_quit_button.visible = content.show_quit and _quit_available

## game_screen.gd calls this once, before the first configure(), with
## `not OS.has_feature("web")` -- a browser tab can't quit itself, so
## get_tree().quit() would be a visible no-op there.
func set_quit_available(available: bool) -> void:
_quit_available = available
2 changes: 1 addition & 1 deletion game/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ run/main_scene="res://main.tscn"
[display]

window/size/viewport_width=520
window/size/viewport_height=600
window/size/viewport_height=545
window/stretch/mode="canvas_items"
window/stretch/aspect="keep"

Expand Down
Loading
Loading