172 lines
7.9 KiB
JavaScript
172 lines
7.9 KiB
JavaScript
// Labyrinth — static data. No Phaser, no game state: just the maze tile
|
||
// vocabulary, the fixed-tile skeleton, the movable-tile bag, the treasure
|
||
// catalog, player home corners, and spritesheet frame helpers. Everything
|
||
// dynamic (the shuffled board, spare tile, pawns, targets) lives in
|
||
// LabyrinthLogic.js.
|
||
|
||
// 7×7 board. The 49 cells hold 48 movable + the fixed skeleton; one extra
|
||
// "spare" tile is always held off-board and pushed in each turn.
|
||
export const GRID = 7;
|
||
|
||
// ── Directions ────────────────────────────────────────────────────────────────
|
||
// Side indices around a tile, clockwise from the top. A clockwise rotation by
|
||
// `r` quarter-turns maps side s → (s + r) % 4.
|
||
export const N = 0, E = 1, S = 2, W = 3;
|
||
export const DIRS = [N, E, S, W];
|
||
export const OPPOSITE = { [N]: S, [E]: W, [S]: N, [W]: E };
|
||
// Row/col delta when stepping out of a tile through a given side.
|
||
export const DELTA = {
|
||
[N]: { dr: -1, dc: 0 },
|
||
[E]: { dr: 0, dc: 1 },
|
||
[S]: { dr: 1, dc: 0 },
|
||
[W]: { dr: 0, dc: -1 },
|
||
};
|
||
|
||
// Base (unrotated) open sides per tile shape.
|
||
// I (straight) — corridor N↔S
|
||
// L (corner) — corridor N↔E
|
||
// T (junction) — open E,S,W (closed N)
|
||
export const BASE_SIDES = {
|
||
I: [N, S],
|
||
L: [N, E],
|
||
T: [E, S, W],
|
||
};
|
||
|
||
// The set of open sides for a tile of `type` rotated `rot` quarter-turns.
|
||
export function openSides(type, rot) {
|
||
return BASE_SIDES[type].map((s) => (s + rot) % 4);
|
||
}
|
||
|
||
// Does a tile of (type,rot) have an opening on `side`?
|
||
export function isOpen(type, rot, side) {
|
||
return openSides(type, rot).includes(side);
|
||
}
|
||
|
||
// ── Treasures ────────────────────────────────────────────────────────────────
|
||
// 24 treasures. The array index IS the spritesheet frame for both the on-tile
|
||
// overlay (labyrinth-treasures, 100×100) and the held card (labyrinth-cards,
|
||
// 270×390). 12 creatures (0–11) then 12 objects (12–23).
|
||
export const TREASURES = [
|
||
{ key: 'dragon', name: 'Dragon' },
|
||
{ key: 'genie', name: 'Genie' },
|
||
{ key: 'ghost', name: 'Ghost' },
|
||
{ key: 'goblin', name: 'Goblin' },
|
||
{ key: 'witch', name: 'Witch' },
|
||
{ key: 'skeleton', name: 'Skeleton' },
|
||
{ key: 'bat', name: 'Bat' },
|
||
{ key: 'owl', name: 'Owl' },
|
||
{ key: 'spider', name: 'Spider' },
|
||
{ key: 'lizard', name: 'Lizard' },
|
||
{ key: 'beetle', name: 'Beetle' },
|
||
{ key: 'rat', name: 'Rat' },
|
||
{ key: 'crown', name: 'Crown' },
|
||
{ key: 'ring', name: 'Ring' },
|
||
{ key: 'keys', name: 'Keys' },
|
||
{ key: 'chest', name: 'Treasure Chest' },
|
||
{ key: 'gem', name: 'Gem' },
|
||
{ key: 'coins', name: 'Gold Coins' },
|
||
{ key: 'candelabra', name: 'Candelabra' },
|
||
{ key: 'grimoire', name: 'Grimoire' },
|
||
{ key: 'sword', name: 'Sword' },
|
||
{ key: 'helmet', name: 'Helmet' },
|
||
{ key: 'map', name: 'Map Scroll' },
|
||
{ key: 'chalice', name: 'Chalice' },
|
||
];
|
||
export const TREASURE_COUNT = TREASURES.length; // 24
|
||
|
||
// ── Fixed skeleton ──────────────────────────────────────────────────────────
|
||
// The 16 tiles anchored at even row/col never move. Corners are L-tiles opening
|
||
// inward; the rest are T-junctions whose closed (flat) side faces the board
|
||
// edge. The 12 non-corner fixed tiles each carry a treasure (frames 0–11).
|
||
export const FIXED = [
|
||
// four corners (no treasure)
|
||
{ r: 0, c: 0, type: 'L', rot: 1 }, // opens E,S
|
||
{ r: 0, c: 6, type: 'L', rot: 2 }, // opens S,W
|
||
{ r: 6, c: 0, type: 'L', rot: 0 }, // opens N,E
|
||
{ r: 6, c: 6, type: 'L', rot: 3 }, // opens N,W
|
||
// top edge — closed N
|
||
{ r: 0, c: 2, type: 'T', rot: 0, treasure: 0 },
|
||
{ r: 0, c: 4, type: 'T', rot: 0, treasure: 1 },
|
||
// bottom edge — closed S
|
||
{ r: 6, c: 2, type: 'T', rot: 2, treasure: 2 },
|
||
{ r: 6, c: 4, type: 'T', rot: 2, treasure: 3 },
|
||
// left edge — closed W
|
||
{ r: 2, c: 0, type: 'T', rot: 3, treasure: 4 },
|
||
{ r: 4, c: 0, type: 'T', rot: 3, treasure: 5 },
|
||
// right edge — closed E
|
||
{ r: 2, c: 6, type: 'T', rot: 1, treasure: 6 },
|
||
{ r: 4, c: 6, type: 'T', rot: 1, treasure: 7 },
|
||
// inner four — point inward
|
||
{ r: 2, c: 2, type: 'T', rot: 3, treasure: 8 }, // open N,E,S
|
||
{ r: 2, c: 4, type: 'T', rot: 1, treasure: 9 }, // open N,S,W
|
||
{ r: 4, c: 2, type: 'T', rot: 3, treasure: 10 },
|
||
{ r: 4, c: 4, type: 'T', rot: 1, treasure: 11 },
|
||
];
|
||
|
||
export function isFixed(r, c) {
|
||
return r % 2 === 0 && c % 2 === 0;
|
||
}
|
||
|
||
// ── Movable tile bag ──────────────────────────────────────────────────────────
|
||
// 34 tiles (33 fill the open cells, 1 is the starting spare). Treasures 12–23
|
||
// ride on twelve of them (the six T's + six L's, matching the classic deck);
|
||
// straights carry none. Rotations are randomized at deal time in the logic.
|
||
export function buildMovableBag() {
|
||
const bag = [];
|
||
const push = (type, treasure = null) => bag.push({ type, treasure });
|
||
|
||
// 6 T-junctions, all treasured (12–17)
|
||
for (let i = 0; i < 6; i++) push('T', 12 + i);
|
||
// 16 corners — six treasured (18–23), ten plain
|
||
for (let i = 0; i < 6; i++) push('L', 18 + i);
|
||
for (let i = 0; i < 10; i++) push('L', null);
|
||
// 12 straights, no treasure
|
||
for (let i = 0; i < 12; i++) push('I', null);
|
||
|
||
return bag; // length 34
|
||
}
|
||
|
||
// ── Players ────────────────────────────────────────────────────────────────
|
||
// Home corners in seat order. Two-player games use opposite corners (seats 0 &
|
||
// 1 are diagonal), three/four fill in the remaining corners.
|
||
export const HOME_CORNERS = [
|
||
{ r: 0, c: 0 },
|
||
{ r: 6, c: 6 },
|
||
{ r: 0, c: 6 },
|
||
{ r: 6, c: 0 },
|
||
];
|
||
|
||
export const PLAYER_COLORS = [0xd0473a, 0x4a90d9, 0x49a25a, 0xe2b53c];
|
||
export const PLAYER_COLOR_HEX = ['#d0473a', '#4a90d9', '#49a25a', '#e2b53c'];
|
||
|
||
// ── Insertion slots ──────────────────────────────────────────────────────────
|
||
// The 12 places the spare can be pushed in: the shiftable rows/cols (1,3,5) on
|
||
// each of the four edges. `side` names where the spare enters; the row/col it
|
||
// affects is `index`. Pushing into a slot ejects the tile at the far end.
|
||
export const SHIFT_LINES = [1, 3, 5];
|
||
export const SLOTS = (() => {
|
||
const out = [];
|
||
for (const i of SHIFT_LINES) out.push({ id: `T${i}`, side: 'top', index: i });
|
||
for (const i of SHIFT_LINES) out.push({ id: `B${i}`, side: 'bottom', index: i });
|
||
for (const i of SHIFT_LINES) out.push({ id: `L${i}`, side: 'left', index: i });
|
||
for (const i of SHIFT_LINES) out.push({ id: `R${i}`, side: 'right', index: i });
|
||
return out;
|
||
})();
|
||
|
||
export const OPPOSITE_SIDE = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' };
|
||
|
||
// The slot whose push would directly reverse the given one (same line, opposite
|
||
// edge). Returns a slot id or null.
|
||
export function reverseSlotId(slot) {
|
||
if (!slot) return null;
|
||
const side = OPPOSITE_SIDE[slot.side];
|
||
return `${side === 'top' ? 'T' : side === 'bottom' ? 'B' : side === 'left' ? 'L' : 'R'}${slot.index}`;
|
||
}
|
||
|
||
// ── Spritesheet frame helpers ──────────────────────────────────────────────
|
||
// Tile background: frame 0 = movable, 1 = fixed/anchored.
|
||
export function tileFrame(fixed) { return fixed ? 1 : 0; }
|
||
// Treasure overlay & card share the treasure index as their frame.
|
||
export function treasureFrame(idx) { return idx; }
|
||
export function cardFrame(idx) { return idx; }
|