Skip to content

mesopelagique/Taquin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Taquin

The 15-puzzle for 4D, sliding on an animation engine.

Built on Hero (cs.hero), a shared element transition component.

The Taquin board

Play

Open the project — On Startup runs PLAY_Taquin. Slide tiles with the arrow keys (the arrow names the direction the tile travels), or click a tile next to the hole. Shuffle deals a new board.

The engine, used with the grain

A taquin move is a tile hopping from one known cell to another — which is exactly what a keyframe interpolator does. One tween per move, easeOutQuad, 130 ms, and that is the entire "physics" of the game:

This.transition.animate("tile_"+String($tile))\
	.to({left: $slot.left; top: $slot.top; width: This.size; height: This.size})\
	.duration(130)\
	.easing("easeOutQuad")\
	.start()

No ray-casting, no smuggling — unlike ArcanoidGame, where continuous motion had to be forced into tween shape. Here the game is discrete hops.

Only one tile moves per move, so a tile can afford to be two objects: the picture and its number badge, tweened together. Two animations per move, nothing more.

Half of all shuffles are a lie

This is the part worth knowing. A random permutation of a 15-puzzle is solvable only half the time. The parity of the permutation, plus the row of the hole, is an invariant that no legal move can change — so shuffling the tiles at random hands the player an impossible board on a coin flip.

So the shuffle here is not a permutation. It walks the hole through 300 random legal moves from the solved board. Every position reachable that way is, by construction, reachable back. No parity test, no re-rolling.

Measured over 20 000 shuffles (the algorithm ported out and checked against the parity invariant):

unsolvable boards 0
corrupted boards 0
accidentally solved 0
tiles out of place 14.0 / 15 average
control: random permutations 49.9 % solvable

The picture

tools/cut.py slices any image into the 15 tiles:

python3 tools/cut.py tools/puzzle.png        # --pad 1.2 to leave more air

Framing is the whole point. A logo sits on a big flat plate, and framing on the plate is what ruins the puzzle: cut the shipped 4D icon at its own bounds and eight of sixteen tiles come out as near-identical blank squares — unsolvable by eye. So the crop frames the coloured subject instead: pixels that are opaque and chromatic, counted per row and column so a shadow's tint or a few antialiased pixels cannot stretch the box. That last part matters more than it sounds — the 4D icon's drop shadow is faintly blue, and a naive bbox stretched 57 px past the logo because of it.

Framed on the subject, the ink fills the frame and only 3 of 15 tiles stay blank-ish (measured: tiles 1, 7 and 13, under 5 % coloured pixels).

It falls back to the alpha bounds, then the whole image, for pictures with no such subject — a photo is chromatic all over and frames on itself, which is correct.

Why the numbers? Even well framed, a few tiles are plain. Numbers are the classic taquin answer, and they cost nothing here since only one tile moves per move. Use a busier picture and you can drop them.

4D can also cut at runtime — TRANSFORM PICTURE($pic; Crop; x; y; w; h) works, and was verified before this was written. Cutting up front was preferred because the pieces are then plain assets that always render, with no runtime picture binding to get wrong.

Layout lives in the form

Cell geometry is read from the slot_1slot_16 objects with cs.hero.ElementState, and the tile side comes from slot_1's width. Move the slots in the form editor and the game follows — no constants to chase.

cs.TaquinGame

Properties Description Type
.board 16 cells: tile number 1–15, or 0 for the hole Collection
.empty Index of the hole Integer
.moves / .won Current game state Integer / Boolean
.slots Cell geometry read from the form Collection
Functions Action
.shuffle () Deals a new — always solvable — board
.reset () Back to the solved picture
.move (direction) "left"/"right"/"up"/"down": the way the tile travels
.slide (cell) Slides the tile in that cell into the hole, if they touch
.tick () Watches for a click. Call on On Timer

Form contract

Store the instance in Form.game, and keep the timer armed — the engine releases it when idle, but tick() still has to watch the mouse:

: (Form event code=On Timer)
	Form.transition.onTimer()
	Form.game.tick()
	SET TIMER(1)

The arrow keys need a decoy

4D has no On Key Down. On After Keystroke works, but — the part that costs you an afternoon — it only fires for an enterable object that currently has the focus. A form full of static pictures has nothing to focus, so the event never fires and the keyboard is dead, silently.

The fix is a decoy: InputZone, a tiny borderless input bound to an empty Form.keyboard, focused on load.

: (Form event code=On Load)
	Form.keyboard:=""
	GOTO OBJECT(*; "InputZone")   // without this, the arrow keys do nothing

Anything that steals the focus — the Shuffle button, for one — kills the keyboard until it is handed back, so the button's handler re-focuses the decoy after it runs.

About

The 15-puzzle for 4D

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors