Repository navigation
feat(arcade): fill snake stage with more 20px cells #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { afterEach, describe, expect, it } from "vitest"; | ||
| import { cleanup, fireEvent, render, screen } from "@testing-library/react"; | ||
| import SnakeGame from "./SnakeGame"; | ||
| import { CELL_CSS, EDGE_INSET } from "./snake/stage"; | ||
|
|
||
| function mockBox(el: HTMLElement, w: number, h: number): void { | ||
| Object.defineProperty(el, "clientWidth", { configurable: true, value: w }); | ||
| Object.defineProperty(el, "clientHeight", { configurable: true, value: h }); | ||
| el.getBoundingClientRect = () => | ||
| ({ | ||
| x: 0, | ||
| y: 0, | ||
| left: 0, | ||
| top: 0, | ||
| right: w, | ||
| bottom: h, | ||
| width: w, | ||
| height: h, | ||
| toJSON: () => ({}), | ||
| }) as DOMRect; | ||
| } | ||
|
|
||
| describe("SnakeGame stage fill", () => { | ||
| afterEach(() => { | ||
| cleanup(); | ||
| }); | ||
|
|
||
| it("starts a scored run at the measured stage size, not a fixed 34x18", () => { | ||
| const cols = 50; | ||
| const rows = 28; | ||
| const w = cols * CELL_CSS + 2 * EDGE_INSET; | ||
| const h = rows * CELL_CSS + 2 * EDGE_INSET; | ||
|
|
||
| const { container } = render( | ||
| <SnakeGame | ||
| seed={1} | ||
| paused={false} | ||
| shake={false} | ||
| onScore={() => {}} | ||
| onGameOver={() => {}} | ||
| />, | ||
| ); | ||
| const root = container.querySelector(".arcade-snake-root"); | ||
| if (!(root instanceof HTMLElement)) throw new Error("snake root missing"); | ||
| mockBox(root, w, h); | ||
| fireEvent(window, new Event("resize")); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: /^normal$/i })); | ||
|
|
||
| const canvas = container.querySelector("canvas"); | ||
| if (!(canvas instanceof HTMLCanvasElement)) throw new Error("canvas missing"); | ||
| expect(canvas.style.width).toBe(`${cols * CELL_CSS}px`); | ||
| expect(canvas.style.height).toBe(`${rows * CELL_CSS}px`); | ||
| expect(canvas.width).toBe(cols * CELL_CSS); | ||
| expect(canvas.height).toBe(rows * CELL_CSS); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { | ||
| CELL_CSS, | ||
| EDGE_INSET, | ||
| MIN_COLS, | ||
| MIN_ROWS, | ||
| boardForStage, | ||
| } from "./stage"; | ||
|
|
||
| describe("boardForStage", () => { | ||
| it("returns null when the stage has no box yet", () => { | ||
| expect(boardForStage(0, 0)).toBeNull(); | ||
| expect(boardForStage(2, 400)).toBeNull(); | ||
| }); | ||
|
|
||
| it("floors to whole 20px cells and never scales the cell size", () => { | ||
| const wide = 34 * CELL_CSS + 2 * EDGE_INSET + 19; | ||
| const tall = 18 * CELL_CSS + 2 * EDGE_INSET + 19; | ||
| expect(boardForStage(wide, tall)).toEqual({ cols: 34, rows: 18 }); | ||
| }); | ||
|
|
||
| it("grows the board when the stage is larger, still at 20px cells", () => { | ||
| const wide = 52 * CELL_CSS + 2 * EDGE_INSET; | ||
| const tall = 30 * CELL_CSS + 2 * EDGE_INSET; | ||
| expect(boardForStage(wide, tall)).toEqual({ cols: 52, rows: 30 }); | ||
| }); | ||
|
|
||
| it("does not drop below the playable minimum", () => { | ||
| expect(boardForStage(MIN_COLS * CELL_CSS, MIN_ROWS * CELL_CSS)).toEqual({ | ||
| cols: MIN_COLS, | ||
| rows: MIN_ROWS, | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * Snake stage geometry. The playfield fills the cabinet with more cells at a | ||
| * FIXED 20 CSS-px size. Cells are never scaled up to fit; that reads as zoom. | ||
| */ | ||
|
|
||
| /** Original game cell size in CSS pixels. Never scaled up to fill the stage. */ | ||
| export const CELL_CSS = 20; | ||
| /** Smallest playable board if the stage is tiny. */ | ||
| export const MIN_COLS = 20; | ||
| export const MIN_ROWS = 12; | ||
| /** | ||
| * Minimum letterbox per side. The stage surface clips with a 10px border | ||
| * radius; anything closer than ~3px to its edge gets a bite taken out of the | ||
| * corner cells, which players read as "the end cell is cut off". | ||
| */ | ||
| export const EDGE_INSET = 4; | ||
|
|
||
| /** Board size in whole cells for the current stage box. */ | ||
| export interface SnakeBoard { | ||
| readonly cols: number; | ||
| readonly rows: number; | ||
| } | ||
|
|
||
| /** | ||
| * How many whole FIXED-size cells fit in the stage. Returns null when the | ||
| * box has not been measured yet (jsdom / hidden). | ||
| */ | ||
| export function boardForStage( | ||
| clientWidth: number, | ||
| clientHeight: number, | ||
| ): SnakeBoard | null { | ||
| const boxW = clientWidth - 2 * EDGE_INSET; | ||
| const boxH = clientHeight - 2 * EDGE_INSET; | ||
| if (boxW < 1 || boxH < 1) return null; | ||
| return { | ||
| cols: Math.max(MIN_COLS, Math.floor(boxW / CELL_CSS)), | ||
| rows: Math.max(MIN_ROWS, Math.floor(boxH / CELL_CSS)), | ||
| }; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Starting a run with
measuredRef.currentmakes the collision boundary, food distribution, and maximum attainable score depend on the player's viewport/fullscreen state, whileArcadeOverlaystill submits every result to the samesnake-v2leaderboard. A player can therefore resize before starting to select anything from the 20×12 minimum to a much larger field, mixing materially different gameplay conditions with existing fixed-34×18 scores; keep ranked dimensions fixed, normalize/version these scores, or make variable-board runs unranked. This also moves a gameplay decision into the renderer despite the repository's pure-rules/thin-renderer invariant.AGENTS.md reference: AGENTS.md:L71-L74
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declining this one. The owner asked to fill the cabinet with the same 20px cells (more cells, no scale/zoom). Restoring a fixed 34x18 scored board, versioning scores, or making larger boards unranked would reverse that intent.
The reducer is still dimension-agnostic:
initSnakealready takes width/height. The renderer only measures how many whole 20px cells fit and passes those numbers in. Scores stay onsnake-v2; the leaderboard is already client-trusted at this phase.