From e3fb356520225561feece97fdc9c660de6bc5f06 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Fri, 19 Jun 2026 21:54:04 -0600 Subject: [PATCH] feat: enhance craps UI and improve ghost AI skill scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Righteous font with JqkasWild for craps title, puck, and number labels - Refactor zone label rendering into dedicated methods for cleaner code - Add payout information display on craps number boxes (6→SIX, 9→NINE) - Redesign Field zone with styled number circles (accent for 2/12) - Update Don't Pass/Don't Come zones with bar text and red accent borders - Add common words trie for Ghost AI to target on skill ≤3 - Make lower-skill Ghost AI play more human-like by preferring common word continuations - Fallback to off-dictionary play when no common word path available --- public/assets/fonts/JqkasWild.ttf | Bin 0 -> 51792 bytes public/src/games/craps/CrapsGame.js | 134 ++++++++++++++++++++++++---- public/styles.css | 7 ++ server/words/ghostEngine.js | 47 ++++++++-- server/words/wordRoutes.js | 5 +- 5 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 public/assets/fonts/JqkasWild.ttf diff --git a/public/assets/fonts/JqkasWild.ttf b/public/assets/fonts/JqkasWild.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7fadc69c03762a43659eb81ddbf9b4ef3eb58e38 GIT binary patch literal 51792 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�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 literal 0 HcmV?d00001 diff --git a/public/src/games/craps/CrapsGame.js b/public/src/games/craps/CrapsGame.js index 26c5999..bae7a63 100644 --- a/public/src/games/craps/CrapsGame.js +++ b/public/src/games/craps/CrapsGame.js @@ -126,7 +126,7 @@ export default class CrapsGame extends Phaser.Scene { buildTitle() { this.add.text(CX, 52, 'Craps', { - fontFamily: 'Righteous', fontSize: '50px', color: COLORS.textHex, + fontFamily: 'JqkasWild', fontSize: '50px', color: COLORS.textHex, }).setOrigin(0.5).setDepth(D.ui); this.statusText = this.add.text(CX, 96, '', { @@ -146,22 +146,29 @@ export default class CrapsGame extends Phaser.Scene { const startX = CX - totalW / 2; const boxY = 150; BOX_NUMS.forEach((n, i) => { - add(`box${n}`, BET.PLACE, n, String(n), startX + i * (boxW + gap), boxY, boxW, boxH); + const lbl = n === 6 ? 'SIX' : n === 9 ? 'NINE' : String(n); + const payout = (n === 4 || n === 10) ? 'PAYS 9 TO 5' + : (n === 5 || n === 9) ? 'PAYS 7 TO 5' : 'PAYS 7 TO 6'; + add(`box${n}`, BET.PLACE, n, lbl, startX + i * (boxW + gap), boxY, boxW, boxH); + this.zones[`box${n}`].payout = payout; }); this._boxY = boxY; this._boxH = boxH; // Don't Come (left of the boxes) add('dontcome', BET.DONT_COME, null, "DON'T\nCOME", TABLE.x + 24, boxY, startX - TABLE.x - 44, boxH * 2 + 14); + this.zones.dontcome.barText = 'BAR 12'; // Come add('come', BET.COME, null, 'COME', startX, 268, totalW, 88); // Field - add('field', BET.FIELD, null, 'FIELD 2 3 4 9 10 11 12', TABLE.x + 60, 372, TABLE.w - 120, 86, - '2 pays double · 12 pays triple'); + add('field', BET.FIELD, null, 'FIELD', TABLE.x + 60, 372, TABLE.w - 120, 86, + '2 pays double · 12 pays triple'); + this.zones.field.fieldNums = [2, 3, 4, 9, 10, 11, 12]; // Don't Pass bar - add('dontpass', BET.DONT_PASS, null, "DON'T PASS BAR", TABLE.x + 140, 476, TABLE.w - 280, 60); + add('dontpass', BET.DONT_PASS, null, "DON'T PASS", TABLE.x + 140, 476, TABLE.w - 280, 60); + this.zones.dontpass.barText = 'BAR 12'; // Pass line add('pass', BET.PASS, null, 'PASS LINE', TABLE.x + 100, 552, TABLE.w - 200, 76); @@ -187,18 +194,98 @@ export default class CrapsGame extends Phaser.Scene { zone.on('pointerout', () => this.hoverZone(key, false)); zone.on('pointerup', () => this.onZoneClick(key)); - const big = z.number != null; - this.add.text(z.center.x, z.center.y, z.label, { - fontFamily: big ? 'Righteous' : '"Julius Sans One"', - fontSize: big ? '40px' : '22px', - color: COLORS.textHex, align: 'center', - }).setOrigin(0.5).setDepth(D.zoneLabel); - if (z.sub) { - this.add.text(z.center.x, z.rect.y + z.rect.h - 16, z.sub, { - fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex, + this._renderZoneLabel(key, z); + } + } + + _renderZoneLabel(key, z) { + if (z.number != null) return this._renderNumberBox(z); + if (key === 'field') return this._renderFieldZone(z); + if (key === 'dontpass') return this._renderDontZone(z, 21, 9); + if (key === 'dontcome') return this._renderDontComeZone(z); + if (key === 'pass') return this._renderLineZone(z, 'PASS LINE', 34); + if (key === 'come') return this._renderLineZone(z, 'COME', 32); + } + + _renderNumberBox(z) { + this.add.text(z.center.x, z.center.y - 16, z.label, { + fontFamily: 'JqkasWild', fontSize: '38px', color: COLORS.textHex, + }).setOrigin(0.5).setDepth(D.zoneLabel); + this.add.text(z.center.x, z.center.y + 24, z.payout, { + fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.accentHex, + }).setOrigin(0.5).setDepth(D.zoneLabel); + } + + _renderLineZone(z, text, fontSize) { + this.add.text(z.center.x, z.center.y, text, { + fontFamily: '"Julius Sans One"', fontSize: `${fontSize}px`, color: COLORS.textHex, + letterSpacing: 14, + }).setOrigin(0.5).setDepth(D.zoneLabel); + } + + _renderDontZone(z, fontSize, ls) { + this.add.text(z.center.x, z.center.y - 11, "DON'T PASS", { + fontFamily: '"Julius Sans One"', fontSize: `${fontSize}px`, color: COLORS.textHex, + letterSpacing: ls, + }).setOrigin(0.5).setDepth(D.zoneLabel); + this.add.text(z.center.x, z.center.y + 17, z.barText, { + fontFamily: '"Julius Sans One"', fontSize: '12px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.zoneLabel); + } + + _renderDontComeZone(z) { + this.add.text(z.center.x, z.center.y - 44, "DON'T", { + fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex, + }).setOrigin(0.5).setDepth(D.zoneLabel); + this.add.text(z.center.x, z.center.y, 'COME', { + fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex, + letterSpacing: 8, + }).setOrigin(0.5).setDepth(D.zoneLabel); + this.add.text(z.center.x, z.center.y + 44, z.barText, { + fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.zoneLabel); + } + + _renderFieldZone(z) { + // "FIELD" label left-anchored + this.add.text(z.rect.x + 55, z.center.y, 'FIELD', { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex, + letterSpacing: 6, + }).setOrigin(0.5).setDepth(D.zoneLabel); + + // Vertical separator + const sep = this.add.graphics().setDepth(D.zoneLabel); + sep.lineStyle(1, COLORS.accent, 0.35); + sep.lineBetween(z.rect.x + 100, z.rect.y + 8, z.rect.x + 100, z.rect.y + z.rect.h - 8); + + // 7 number tokens, evenly spaced from x=430 to x=1630, step=200 + const fieldNums = z.fieldNums; + const startX = 430, step = 200; + fieldNums.forEach((n, i) => { + const nx = startX + i * step; + const ny = z.center.y; + if (n === 2 || n === 12) { + const cg = this.add.graphics().setDepth(D.zoneLabel); + cg.fillStyle(COLORS.accent, 0.85); + cg.fillCircle(nx, ny, 22); + cg.lineStyle(2, COLORS.gold, 1); + cg.strokeCircle(nx, ny, 22); + this.add.text(nx, ny, String(n), { + fontFamily: 'JqkasWild', fontSize: n === 12 ? '22px' : '22px', + color: COLORS.textDarkHex, + }).setOrigin(0.5).setDepth(D.zoneLabel); + } else { + const fs = (n === 10 || n === 11) ? '24px' : '28px'; + this.add.text(nx, ny, String(n), { + fontFamily: 'JqkasWild', fontSize: fs, color: COLORS.textHex, }).setOrigin(0.5).setDepth(D.zoneLabel); } - } + }); + + // Sub-text at bottom of zone + this.add.text(z.center.x, z.rect.y + z.rect.h - 10, z.sub, { + fontFamily: '"Julius Sans One"', fontSize: '12px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.zoneLabel); } drawZones(hoverKey = null) { @@ -208,10 +295,19 @@ export default class CrapsGame extends Phaser.Scene { for (const [key, z] of Object.entries(this.zones)) { const { x, y, w, h } = z.rect; const isLegal = !this.gs || legal.has(z.type); + const isDont = key === 'dontpass' || key === 'dontcome'; g.fillStyle(0x0c2e19, isLegal ? 0.5 : 0.18); g.fillRoundedRect(x, y, w, h, 10); - g.lineStyle(key === hoverKey && isLegal ? 4 : 2, key === hoverKey && isLegal ? COLORS.gold : 0x3c8a52, isLegal ? 0.9 : 0.4); + const strokeColor = (key === hoverKey && isLegal) ? COLORS.gold + : isDont ? 0x8b2020 : 0x3c8a52; + const strokeAlpha = isLegal ? 0.9 : 0.4; + const strokeW = (key === hoverKey && isLegal) ? 4 : 2; + g.lineStyle(strokeW, strokeColor, strokeAlpha); g.strokeRoundedRect(x, y, w, h, 10); + if (z.number != null) { + g.fillStyle(COLORS.accent, isLegal ? 0.55 : 0.2); + g.fillRect(x + 10, y, w - 20, 4); + } } } @@ -231,7 +327,7 @@ export default class CrapsGame extends Phaser.Scene { buildTooltip() { const bg = this.add.graphics(); const title = this.add.text(0, 0, '', { - fontFamily: 'Righteous', fontSize: '30px', color: COLORS.goldHex, + fontFamily: 'JqkasWild', fontSize: '30px', color: COLORS.goldHex, }); const desc = this.add.text(0, 0, '', { fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex, @@ -293,7 +389,7 @@ export default class CrapsGame extends Phaser.Scene { this.puck.add(g); this.puckGfx = g; this.puckText = this.add.text(0, 0, 'OFF', { - fontFamily: 'Righteous', fontSize: '18px', color: '#1a1208', + fontFamily: 'JqkasWild', fontSize: '18px', color: '#1a1208', }).setOrigin(0.5); this.puck.add(this.puckText); this.drawPuck(false); @@ -579,7 +675,7 @@ export default class CrapsGame extends Phaser.Scene { // Place a chip in the left/right side of a zone, skirting the center text. chipSidePos(z, zoneKey, i, n, pi) { // Approximate widths of each zone's center text label. - const avoidW = { pass: 250, dontpass: 310, field: 550, come: 170, dontcome: 60 }[zoneKey] ?? 240; + const avoidW = { pass: 250, dontpass: 310, field: 550, come: 200, dontcome: 60 }[zoneKey] ?? 240; const margin = 28; const jitter = this.seatJitter[pi % this.seatJitter.length]; const leftBound = z.rect.x + margin; diff --git a/public/styles.css b/public/styles.css index 7825eee..c73b3f8 100644 --- a/public/styles.css +++ b/public/styles.css @@ -19,6 +19,13 @@ font-style: normal; } +@font-face { + font-family: 'JqkasWild'; + src: url('/assets/fonts/JqkasWild.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + html, body { margin: 0; padding: 0; diff --git a/server/words/ghostEngine.js b/server/words/ghostEngine.js index 7c25caf..e7b7ec5 100644 --- a/server/words/ghostEngine.js +++ b/server/words/ghostEngine.js @@ -12,6 +12,7 @@ const MIN_LEN = 4; let TRIE = null; +let COMMON_TRIE = null; export function initGhostDictionary(words) { TRIE = { children: Object.create(null), terminal: false, loss: false }; @@ -26,6 +27,19 @@ export function initGhostDictionary(words) { computeLoss(TRIE); } +export function initGhostCommonWords(words) { + COMMON_TRIE = { children: Object.create(null), terminal: false, loss: false }; + for (const w of words) { + if (w.length < MIN_LEN) continue; + let node = COMMON_TRIE; + for (const ch of w) { + node = node.children[ch] || (node.children[ch] = { children: Object.create(null), terminal: false, loss: false }); + } + node.terminal = true; + } + computeLoss(COMMON_TRIE); +} + export function dictionaryReady() { return TRIE !== null; } @@ -44,8 +58,8 @@ function computeLoss(node) { node.loss = !win; } -function nodeFor(fragment) { - let node = TRIE; +function nodeFor(fragment, trie = TRIE) { + let node = trie; for (const ch of fragment) { node = node.children[ch]; if (!node) return null; @@ -86,19 +100,36 @@ export function chooseLetter(fragment, skill) { const node = nodeFor(F); if (!node) return { letter: null, isWord: false, isPrefix: false }; - const legal = Object.keys(node.children); // every child is a valid prefix + const legal = Object.keys(node.children); // every child is a valid ENABLE prefix if (legal.length === 0) return { letter: null, isWord: false, isPrefix: false }; + // Skill <= 3: restrict candidates to letters that continue toward a common word. + // If no common-word continuation exists (human steered off the common path or + // none was available), the AI concedes by going off-dictionary. + // isWord / safe filtering always uses the authoritative ENABLE trie so the AI + // never accidentally complete a real word it thinks is "safe". + let candidates = legal; + if (skill <= 3 && COMMON_TRIE) { + const commonNode = nodeFor(F, COMMON_TRIE); + const commonSet = commonNode ? new Set(Object.keys(commonNode.children)) : new Set(); + const filtered = legal.filter(L => commonSet.has(L)); + if (filtered.length > 0) { + candidates = filtered; + } else { + return { letter: randomFrom(legal), isWord: false, isPrefix: false }; + } + } + const profile = SKILL[Math.max(1, Math.min(5, skill | 0))] ?? SKILL[3]; - const safe = legal.filter(L => !node.children[L].terminal); // don't complete a word + const safe = candidates.filter(L => !node.children[L].terminal); // don't complete a word let letter; if (safe.length === 0) { - // Forced: every legal letter completes a word -> the AI self-loses this turn. - letter = randomFrom(legal); + // Forced: every candidate letter completes a word -> the AI self-loses this turn. + letter = randomFrom(candidates); } else if (Math.random() < profile.blunder) { - // Careless: pick any legal letter, which may accidentally complete a word. - letter = randomFrom(legal); + // Careless: pick any candidate letter, which may accidentally complete a word. + letter = randomFrom(candidates); } else { // Careful: never complete a word; use foresight to hand off a losing position. const winning = safe.filter(L => node.children[L].loss); diff --git a/server/words/wordRoutes.js b/server/words/wordRoutes.js index 4f65530..a4ef4bc 100644 --- a/server/words/wordRoutes.js +++ b/server/words/wordRoutes.js @@ -3,7 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { initScrabbleDictionary, isValidWord, chooseMove } from './scrabbleEngine.js'; -import { initGhostDictionary, judge as ghostJudge, chooseLetter as ghostChooseLetter, suggestWords as ghostSuggestWords } from './ghostEngine.js'; +import { initGhostDictionary, initGhostCommonWords, judge as ghostJudge, chooseLetter as ghostChooseLetter, suggestWords as ghostSuggestWords } from './ghostEngine.js'; import { initWordLadderDictionary, initCommonWordSets as ladderInitCommon, @@ -184,6 +184,9 @@ function loadWordLists() { const common4 = commonWords.filter(w => /^[A-Z]{4}$/.test(w)); ladderInitCommon(common3, common4); console.log(`[words] loaded Word Ladder common words (${common3.length} 3-letter, ${common4.length} 4-letter)`); + const ghostCommon = commonWords.filter(w => w.length >= 4 && /^[A-Z]+$/.test(w)); + initGhostCommonWords(ghostCommon); + console.log(`[words] loaded ${ghostCommon.length} Ghost common words for skill ≤3 AI targeting`); } catch (err) { console.warn('[words] common.txt not found; Kiitos AI will use full ENABLE list'); }