fertig-classic-games/public/src/games/rummikub/RummikubData.js

114 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Rummikub — static data, theme and table geometry. No Phaser, no game state:
// imported by the solver, the logic engine, the AI, the Phaser scene and the
// headless verify harness alike.
//
// Classic Rummikub: 106 tiles (numbers 113 in four colours, two copies each =
// 104, plus two jokers). 24 players, 14 tiles per rack, race to empty your rack
// by laying down valid sets (groups & runs). First table play must total ≥30.
export const ICON_FRAME = 68;
// ── Rules ────────────────────────────────────────────────────────────────────
export const COLORS_ORDER = ['red', 'blue', 'black', 'orange'];
export const MIN_NUM = 1;
export const MAX_NUM = 13;
export const COPIES = 2; // two copies of every numbered tile
export const JOKER_COUNT = 2; // two jokers
export const TILE_COUNT = 106; // 13 × 4 × 2 + 2
export const RACK_START = 14; // tiles dealt to each player
export const INITIAL_MELD_MIN = 30; // a player's first table play must total ≥30
export const JOKER_PENALTY = 30; // a joker left on the rack at game end
export const MIN_SET = 3; // a valid group/run is at least three tiles
export const MAX_PLAYERS = 4;
// ── Tiles ────────────────────────────────────────────────────────────────────
// A tile is a plain descriptor: { id, color, number, isJoker }. A joker carries
// color:null / number:null until it is "represented" inside a committed set; the
// solver attaches a transient { rcolor, rnumber } assignment for display only.
/** Numeric value of a tile for scoring / the initial-meld minimum. Jokers are
* contextual and must be valued via the solver's set assignment. */
export function tileValue(tile) {
if (tile.isJoker) return 0;
return tile.number;
}
/** Display hex for a tile's number, by colour. */
export function colorHex(color) {
switch (color) {
case 'red': return '#d23b32';
case 'blue': return '#2f6fb0';
case 'black': return '#222018';
case 'orange': return '#e08a1e';
default: return '#222018';
}
}
/** Stable rack sort: by colour order, then number; jokers last. */
export function sortRackTiles(tiles) {
const ci = (c) => { const i = COLORS_ORDER.indexOf(c); return i < 0 ? 99 : i; };
return tiles.slice().sort((a, b) => {
if (a.isJoker !== b.isJoker) return a.isJoker ? 1 : -1;
if (a.isJoker && b.isJoker) return 0;
return ci(a.color) - ci(b.color) || a.number - b.number;
});
}
// ── Theme (classic ivory tiles, warm wood rack, green felt) ───────────────────
export const THEME = {
feltTop: 0x1c6b3a, // lit centre of the felt
feltMid: 0x125230, // mid green
feltEdge: 0x07351c, // deep green at the edge
rail: 0x5a3a1c, // walnut wood rack
railHi: 0x7a5230, // lit wood grain
railDark: 0x32200f, // wood shadow
brass: 0xc9a227, // brass trim
brassHi: 0xf0d77a, // brass highlight
gold: 0xd4a017,
tileFace: 0xf6efdb, // cream ivory tile face
tileFaceHi:0xfffaf0, // tile sheen
tileSide: 0xb9ae90, // extruded side / shadow
tileEdge: 0x9c8f6a, // tile border
tileSel: 0xfff4cf, // lifted/selected tile tint
validGlow: 0x55d96a, // a complete, valid set
invalidGlow: 0xe0556a, // an incomplete / illegal set
trayFill: 0x0c3f22, // table set backing tray
trayEdge: 0x1f6b3c,
text: 0xf2ead8,
jokerHex: '#8e44ad',
};
// ── Table geometry ────────────────────────────────────────────────────────────
// Seat 0 is always the human (full face-up rack along the bottom). The AI seats
// fill the top, then left, then right depending on the player count. Returns the
// anchor each seat's face-down stack + count label is drawn at, plus the central
// table band and the human rack strip.
export function buildTableLayout(playerCount, { width, height }) {
const cx = width / 2;
const seats = {
0: { x: cx, y: height - 150, dir: 'h', side: 'bottom' },
};
// AI seats by count: 2p → top; 3p → top + left; 4p → top + left + right.
const order = [];
if (playerCount >= 2) order.push({ seat: 1, x: cx, y: 200, dir: 'h', side: 'top' });
if (playerCount >= 3) order.push({ seat: 2, x: 150, y: 470, dir: 'v', side: 'left' });
if (playerCount >= 4) order.push({ seat: 3, x: width - 150, y: 470, dir: 'v', side: 'right' });
for (const o of order) seats[o.seat] = o;
// Side opponents at x≈150 (left) and x≈1770 (right) need clear space.
// Portrait ring + name text extends ~180px inward from each side opponent.
const lm = playerCount >= 3 ? 350 : 120; // left margin
const rm = playerCount >= 4 ? 350 : 120; // right margin
const tableX = lm, tableY = 300, tableW = width - lm - rm, tableH = 430;
return {
cx,
seats,
// Pool sits just outside the table band to the right, near the bottom edge.
pool: { x: tableX + tableW + 70, y: tableY + tableH - 20 },
table: { x: tableX, y: tableY, w: tableW, h: tableH },
rack: { x: 120, y: height - 168, w: width - 240, h: 150 },
};
}