Skip to content

jengine-grid

Grid games: a self-drawing cell board with layouts, and a snake that steps on it.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-grid
Version0.1.0
Entry pointjengine_grid::GridPackage
Script namespaceGrid
Listed byprojects/arcade-classics, projects/engine-test-lab

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.

Scene keyRust type
grid_boardGridBoardpackage only: needs a v2 scene
grid_snakeGridSnakepackage only: needs a v2 scene
grid_stackerGridStackerpackage only: needs a v2 scene
grid_match3GridMatch3package only: needs a v2 scene

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.

CallFlat 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
  • projects/arcade-classics/assets/scenes/match3.scene
  • projects/arcade-classics/assets/scenes/snake.scene
  • projects/arcade-classics/assets/scenes/tetris.scene
  • projects/engine-test-lab/assets/scenes/70-grid-snake.scene
  • projects/engine-test-lab/assets/scenes/71-stacker.scene
  • projects/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.

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 value v; a value with no palette entry is transparent.
  • Coordinates: x runs left to right and y bottom to top, both from 0, the same way up as the world.
  • layout is 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 __white unless the board names a texture_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.

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_axis on 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.

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_axis on 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) for clear_delay before 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.

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.
  • ai plays 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.

CallDoes
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)

Each snake’s SnakeState custom component, rewritten every frame:

FieldMeaning
eventsthis 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, dirthe snake now

Each stacker’s StackerState, rewritten every frame:

FieldMeaning
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, clearingthe 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:

FieldMeaning
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, hintthe 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 }.

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 through Grid.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.