jengine-grid
Grid games: a self-drawing cell board with layouts, and a snake that steps on it.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-grid |
| Version | 0.1.0 |
| Entry point | jengine_grid::GridPackage |
| Script namespace | Grid |
| Listed by | projects/arcade-classics, projects/engine-test-lab |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-grid", "source": { "type": "path", "path": "../../packages/jengine-grid" } } ]}Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the
editor’s Rebuild viewport banner). Native and web builds link it on their own.
A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field
for a package component and keeps it as inert custom data.
A script can check that the package is linked with if Grid == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
grid_board | GridBoard | package only: needs a v2 scene |
grid_snake | GridSnake | package only: needs a v2 scene |
grid_stacker | GridStacker | package only: needs a v2 scene |
grid_match3 | GridMatch3 | package only: needs a v2 scene |
Script commands
Section titled “Script commands”Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.
| Call | Flat name |
|---|---|
Grid.grid_set(e, x, y, value) | grid_set |
Grid.grid_fill(e, value) | grid_fill |
Grid.grid_reset(e) | grid_reset |
Grid.grid_load(e, rows) | grid_load |
Grid.grid_set_color(e, value, r, g, b, a) | grid_set_color |
Grid.snake_reset(e, x, y, dir) | snake_reset |
Grid.snake_turn(e, dir) | snake_turn |
Grid.snake_set_speed(e, step_time) | snake_set_speed |
Grid.snake_set_ai(e, on) | snake_set_ai |
Grid.stacker_rotate(e, dir) | stacker_rotate |
Grid.stacker_hard_drop(e) | stacker_hard_drop |
Grid.stacker_hold(e) | stacker_hold |
Grid.stacker_reset(e) | stacker_reset |
Grid.stacker_set_gravity(e, seconds) | stacker_set_gravity |
Grid.stacker_set_ai(e, on) | stacker_set_ai |
Grid.match3_click(e, wx, wy) | match3_click |
Grid.match3_drag(e, wx0, wy0, wx1, wy1) | match3_drag |
Grid.match3_cursor(e, dx, dy) | match3_cursor |
Grid.match3_press(e) | match3_press |
Grid.match3_reset(e) | match3_reset |
Grid.match3_set_ai(e, on) | match3_set_ai |
Grid.match3_set_input(e, on) | match3_set_input |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/match3.sceneprojects/arcade-classics/assets/scenes/snake.sceneprojects/arcade-classics/assets/scenes/tetris.sceneprojects/engine-test-lab/assets/scenes/70-grid-snake.sceneprojects/engine-test-lab/assets/scenes/71-stacker.sceneprojects/engine-test-lab/assets/scenes/72-match3.scene
From packages/jengine-grid/README.md.
Grid games: a board of cells that draws itself, a snake that moves on it, a falling-block stacker, and a swap-and-match board. Snake, Tetris and Match-3 are built on it; Minesweeper and a Tron mode are the ones it’s meant for next.
Used by projects/arcade-classics (Snake, Tetris, Match-3) and Engine Test
Lab scenes 70-72.
The board: grid_board
Section titled “The board: grid_board”A cols x rows board of integer cells, centred on its entity. On the first
frame of play the package creates one sprite entity per cell, and after that
it only recolours cells whose value changed. The scene holds one entity for
the whole board, and no script manages hundreds of sprites.
"grid_board": { "cols": 30, "rows": 20, "cell_size": 28, "gap": 3, "z_order": 1, "palette": [[0.05,0.07,0.09,1], [0.42,0.47,0.58,1], [0.22,0.78,0.36,1]], "layout": ["##############", "#............#", "..."]}- Values are small integers.
palette[v]is the colour of valuev; a value with no palette entry is transparent. - Coordinates:
xruns left to right andybottom to top, both from 0, the same way up as the world. layoutis written top row first, like a text file: a digit is that value,#is 1, and anything else is 0.- Texture: cells use the engine’s built-in
__whiteunless the board names atexture_id. - Editor: the cells exist only in play, so the Scene view shows nothing where the board is. Give the entity a backdrop sprite if that matters.
The snake: grid_snake
Section titled “The snake: grid_snake”A snake stepping on a board: the board it names in board, or the board on
its own entity. It writes itself into the board (head_value, body_value),
so it draws through the board. Every non-empty, non-food cell is solid to it:
walls, its own body, another snake.
"grid_snake": { "board": "Board", "start": [6, 10], "dir": "Right", "length": 4, "step_time": 0.12, "wrap": false, "grow_per_food": 1, "auto_food": true, "ai": false }- Turning: from an
input_axison the same entity, one turn per new direction pressed (holding a key doesn’t repeat). A reversal is ignored, and two quick turns between steps both happen. - Food: with
auto_food, one food is always on the board. It’s placed on spawn, and a new one appears whenever the last is eaten. ai: it steers itself toward the nearest food, never into certain death while another way exists, and never into a pocket smaller than itself. That’s enough for an attract mode that plays for minutes.
The stacker: grid_stacker
Section titled “The stacker: grid_stacker”A falling-block well - Tetris - on a board: the one it names in board, or
its own. It owns the locked cells and draws the board each frame (well,
then ghost, then the falling piece), and draws the next queue and the held
piece into two more boards if it names them.
"grid_stacker": { "board": "Well", "next_board": "Next", "hold_board": "Hold", "previews": 3, "gravity": 1.0, "soft_drop": 20, "das": 0.17, "arr": 0.05, "lock_delay": 0.5, "lock_resets": 15, "clear_delay": 0.3, "ai": false }It plays by the modern guideline, because that’s what makes a stacker feel right:
- SRS rotation with the standard wall-kick tables.
- A 7-bag randomizer: every piece once per seven.
- DAS / ARR in Rust from an
input_axison the same entity (x shifts, negative y soft-drops), so auto-repeat is frame-accurate. Rotate, hard drop and hold are edges, sent as commands. - Lock delay with a capped number of resets.
- Hold (once per piece), next previews and a ghost.
- Full rows flash (
flash_value) forclear_delaybefore they collapse. - T-spins by the three-corner rule.
ai: picks the placement that leaves the best well - aggregate height, lines, holes and bumpiness - and walks the piece there. It cleared 103 lines from 270 pieces in two minutes of the arcade’s attract mode.
Piece cell values are 1-7 (I, O, T, S, Z, J, L); ghost_value (8) and
flash_value (9) complete the palette. Scoring and levels are left to the
game: the events carry everything they need.
Swap-and-match: grid_match3
Section titled “Swap-and-match: grid_match3”A Match-3 board that owns one sprite per gem and animates it: a swap
slides, a match shrinks away, and new gems fall in from above. That’s why it
isn’t a grid_board, whose cells can only change colour. It’s centred on its
entity; the scene can put a grid_board checkerboard under it.
"grid_match3": { "cols": 8, "rows": 8, "kinds": 6, "cell_size": 72, "gem_size": 60, "textures": ["sprites/gems/gem_1.png", "..."], "colors": [[0.95,0.3,0.3,1], "..."], "line_h_texture": "...", "line_v_texture": "...", "bomb_texture": "...", "select_texture": "...", "cursor_texture": "...", "ai": false, "input": true }The rules, as the genre has settled them:
- 3+ of a kind in a row or column match; runs that touch (an L or a T) are one match.
- Four in a row leaves a line gem (horizontal run: clears its row; vertical: its column). Five in a row, or an L / T, leaves a bomb that clears the 3 x 3 around it. A special is left on the cell the player swapped, and specials caught in a clear set each other off.
- A swap that makes no match slides back.
- Every clear collapses and refills, and the board is checked again; each round is one step of the chain.
- A new board never starts matched, and a board with no move left reshuffles.
aiplays the move that clears the most, counting specials.
Input goes in as world positions. Grid.match3_click(e, wx, wy) on press
selects a gem or swaps it with an adjacent selection;
Grid.match3_drag(e, wx0, wy0, wx1, wy1) on release swaps in the drag’s
direction (a short drag counts as a click). So a script forwards
Input.get_mouse_world_position() and nothing else. For a keyboard or pad,
Grid.match3_cursor(e, dx, dy) and Grid.match3_press(e) drive a cursor.
Specials are drawn as an overlay texture over the gem; with no overlay texture configured, a special gets no overlay.
Script commands
Section titled “Script commands”| Call | Does |
|---|---|
Grid.grid_set(e, x, y, value) | set one cell |
Grid.grid_fill(e, value) | set every cell |
Grid.grid_reset(e) | back to the layout |
Grid.grid_load(e, rows) | new layout (a list of row strings, top first) and apply it |
Grid.grid_set_color(e, value, r, g, b, a) | recolour every cell holding value |
Grid.snake_reset(e, x, y, dir) | take the snake off the board; it respawns next frame (x, y, dir optional) |
Grid.snake_turn(e, dir) | queue "up", "down", "left" or "right" |
Grid.snake_set_speed(e, step_time) | seconds per step |
Grid.snake_set_ai(e, on) | let it steer itself, or hand it back to input |
Grid.stacker_rotate(e, dir) | rotate clockwise (1) or anticlockwise (-1), with kicks |
Grid.stacker_hard_drop(e) | drop and lock |
Grid.stacker_hold(e) | swap with the held piece, once per piece |
Grid.stacker_reset(e) | empty the well and start over |
Grid.stacker_set_gravity(e, seconds) | seconds per row |
Grid.stacker_set_ai(e, on) | let it play itself |
Grid.match3_click(e, wx, wy) | select, deselect, or swap with the selection |
Grid.match3_drag(e, wx0, wy0, wx1, wy1) | swap in the drag’s direction |
Grid.match3_cursor(e, dx, dy), Grid.match3_press(e) | keyboard / pad cursor |
Grid.match3_reset(e) | a new board, dropped in from above |
Grid.match3_set_ai(e, on), Grid.match3_set_input(e, on) | let it play itself; stop taking moves (a game ended) |
What scripts read
Section titled “What scripts read”Each snake’s SnakeState custom component, rewritten every frame:
| Field | Meaning |
|---|---|
events | this frame’s { kind, x, y, wx, wy, cause }: kind is "ate" or "died", wx/wy is the cell’s world position (for effects), and cause is "wall", "self" or "edge" |
alive, length, eaten, head_x, head_y, dir | the snake now |
Each stacker’s StackerState, rewritten every frame:
| Field | Meaning |
|---|---|
events | { kind, piece, count, tspin, wx, wy }: kind is spawn, shift, rotate, hold, hard_drop (count = cells), lock (count = soft-dropped cells), clear (count = rows, possibly 0 for a T-spin) or game_over; wx/wy is where, in world space |
alive, lines, pieces, hold, next, clearing | the well now; hold and next as piece letters |
Events from commands (a rotate, a hard drop) are reported in the next frame’s batch together with what they caused - the lock, the clear.
Each match-3 board’s Match3State, rewritten every frame:
| Field | Meaning |
|---|---|
events | { kind, count, chain, gem, special, wx, wy }: kind is swap (a move was made), invalid (it slid back), match (count = gems, chain = which round, special = what it left behind), special (one went off), reshuffle, or settled (the move is over; count = the chain it reached) |
idle, moves, chain, selected, hint | the board now; hint is a move as [x1, y1, x2, y2] in world space, or null |
Events are replaced each frame, so read them every frame with
script = { tick_rate = 60 }.
Where it runs
Section titled “Where it runs”The snake, stacker and match-3 systems run at ScriptExec, after the frame’s
script commands. The drawing system runs at PreRender, so every change made this
frame is drawn.
Like every package, it runs natively and in a project-built editor viewport,
and not in web builds.
src/board.rs,src/snake.rs: layouts, coordinates, stepping, growth, every way to die, tail-chasing, wrapping, turn queueing, and an AI that must survive 300 steps and eat 15 times.tests/snake_scene.rs: the arcade’s real Snake scene. The board creates 600 correctly coloured sprites, the AI eats for a minute, and a level loads throughGrid.grid_load/Grid.snake_reset/Grid.grid_set_color.src/stacker.rs: piece shapes and rotations, the bag, spawn and gravity, hard drop, line clears, wall kicks, lock delay and its reset cap, DAS, hold, T-spins (and a plain drop not being one), topping out, and an AI that must clear 90 lines in 300 pieces.tests/tetris_scene.rs: the arcade’s Tetris scene - all three boards draw, the AI clears lines for two minutes, and hold / rotate / hard drop round-trip through commands and events.src/match3.rs: runs, L-shapes, line and bomb specials and their chain reactions, collapse and refill, fresh boards (never matched, always a move), swap validity, clicks and selection, and a whole AI-played game.tests/match3_scene.rs: the arcade’s Match-3 scene. The AI plays it and every gem is drawn; a player move sent as a click and a drag in world space plays out; a swap that matches nothing slides back and changes nothing.tests/test_lab_scene.rs: test-lab scenes 70-72.