feat(rummikub): add rack reordering with insert indicator and improve layout
- Implement drag-and-drop rack reordering with animated slot indicator showing where a tile will be inserted - Preserve manual rack ordering across turn resets and draw actions - Support dynamic table layout with proper margins for side opponents - Reposition player portraits, buttons, and UI elements for 3-4 player configurations - Add point value labels below valid committed sets containing staged tiles - Scale up tiles slightly when picked up for drag feedback
This commit is contained in:
parent
f2da9bfb20
commit
6927b148f2
|
|
@ -90,17 +90,24 @@ export function buildTableLayout(playerCount, { width, height }) {
|
||||||
};
|
};
|
||||||
// AI seats by count: 2p → top; 3p → top + left; 4p → top + left + right.
|
// AI seats by count: 2p → top; 3p → top + left; 4p → top + left + right.
|
||||||
const order = [];
|
const order = [];
|
||||||
if (playerCount >= 2) order.push({ seat: 1, x: cx, y: 150, dir: 'h', side: 'top' });
|
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 >= 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' });
|
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;
|
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 {
|
return {
|
||||||
cx,
|
cx,
|
||||||
seats,
|
seats,
|
||||||
pool: { x: cx, y: 232 }, // face-down draw pool anchor
|
// Pool sits just outside the table band to the right, near the bottom edge.
|
||||||
// Central band where committed sets are laid out (top-left origin, wraps).
|
pool: { x: tableX + tableW + 70, y: tableY + tableH - 20 },
|
||||||
table: { x: 120, y: 300, w: width - 240, h: 430 },
|
table: { x: tableX, y: tableY, w: tableW, h: tableH },
|
||||||
rack: { x: 120, y: height - 168, w: width - 240, h: 150 },
|
rack: { x: 120, y: height - 168, w: width - 240, h: 150 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.recorded = false;
|
this.recorded = false;
|
||||||
this.dragging = null;
|
this.dragging = null;
|
||||||
|
this.rackDragState = null;
|
||||||
this.originalTableIds = new Set();
|
this.originalTableIds = new Set();
|
||||||
|
|
||||||
this.tableObjs = [];
|
this.tableObjs = [];
|
||||||
|
|
@ -67,7 +68,7 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
this.buildBackdrop();
|
this.buildBackdrop();
|
||||||
this.buildPortraits();
|
this.buildPortraits();
|
||||||
|
|
||||||
this.statusText = this.add.text(GAME_WIDTH / 2, 264, '', {
|
this.statusText = this.add.text(GAME_WIDTH / 2, 780, '', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex,
|
fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(D.ui).setShadow(0, 2, '#000', 6);
|
}).setOrigin(0.5).setDepth(D.ui).setShadow(0, 2, '#000', 6);
|
||||||
|
|
||||||
|
|
@ -106,7 +107,7 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
fontFamily: 'Righteous', fontSize: '32px', color: HEX(THEME.brassHi),
|
fontFamily: 'Righteous', fontSize: '32px', color: HEX(THEME.brassHi),
|
||||||
}).setOrigin(0.5, 0).setDepth(D.ui).setAlpha(0.6);
|
}).setOrigin(0.5, 0).setDepth(D.ui).setAlpha(0.6);
|
||||||
|
|
||||||
this.add.text(this.layout.table.x - 16, this.layout.table.y - 24, 'TABLE', {
|
this.add.text(this.layout.table.x - 16, this.layout.table.y - 34, 'TABLE', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
|
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
|
||||||
}).setOrigin(0, 1).setDepth(D.ui);
|
}).setOrigin(0, 1).setDepth(D.ui);
|
||||||
}
|
}
|
||||||
|
|
@ -114,17 +115,26 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
buildPortraits() {
|
buildPortraits() {
|
||||||
for (let seat = 0; seat < this.playerCount; seat++) {
|
for (let seat = 0; seat < this.playerCount; seat++) {
|
||||||
const a = this.layout.seats[seat];
|
const a = this.layout.seats[seat];
|
||||||
const px = a.side === 'bottom' ? 80 : a.side === 'top' ? 80 : a.x;
|
const px = a.side === 'bottom' ? 80 : a.x;
|
||||||
const py = a.side === 'bottom' ? a.y : a.side === 'top' ? a.y : a.y - 110;
|
const py = a.side === 'bottom' ? a.y : a.y - 80;
|
||||||
this.portraits[seat] = seat === 0
|
this.portraits[seat] = seat === 0
|
||||||
? createPlayerPortrait(this, px, py, 42, D.ui, 'RummikubGame')
|
? createPlayerPortrait(this, px, py, 42, D.ui, 'RummikubGame')
|
||||||
: createOpponentPortrait(this, this.seatOpp[seat], px, py, 42, D.ui, { playIntro: seat === 1 });
|
: createOpponentPortrait(this, this.seatOpp[seat], px, py, 42, D.ui, { playIntro: seat === 1 });
|
||||||
this.add.text(px + 58, py - 8, this.seatName[seat].toUpperCase(), {
|
// Left/right opponents: name + count stacked above the portrait circle.
|
||||||
fontFamily: 'Righteous', fontSize: '18px', color: HEX(THEME.brassHi),
|
// Top/bottom opponents: name + count to the right of the portrait.
|
||||||
}).setOrigin(0, 0.5).setDepth(D.ui);
|
const sideOpponent = a.side === 'left' || a.side === 'right';
|
||||||
this.portraits[seat].countText = this.add.text(px + 58, py + 16, '', {
|
this.add.text(
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
sideOpponent ? px : px + 58,
|
||||||
}).setOrigin(0, 0.5).setDepth(D.ui);
|
sideOpponent ? py - 48 : py - 8,
|
||||||
|
this.seatName[seat].toUpperCase(),
|
||||||
|
{ fontFamily: 'Righteous', fontSize: '18px', color: HEX(THEME.brassHi) },
|
||||||
|
).setOrigin(sideOpponent ? 0.5 : 0, sideOpponent ? 1 : 0.5).setDepth(D.ui);
|
||||||
|
this.portraits[seat].countText = this.add.text(
|
||||||
|
sideOpponent ? px : px + 58,
|
||||||
|
sideOpponent ? py - 70 : py + 16,
|
||||||
|
'',
|
||||||
|
{ fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex },
|
||||||
|
).setOrigin(sideOpponent ? 0.5 : 0, sideOpponent ? 1 : 0.5).setDepth(D.ui);
|
||||||
const ring = this.add.graphics().setDepth(D.ring).setVisible(false);
|
const ring = this.add.graphics().setDepth(D.ring).setVisible(false);
|
||||||
ring.lineStyle(5, THEME.brassHi, 0.95); ring.strokeCircle(px, py, 48);
|
ring.lineStyle(5, THEME.brassHi, 0.95); ring.strokeCircle(px, py, 48);
|
||||||
this.portraits[seat].ring = ring;
|
this.portraits[seat].ring = ring;
|
||||||
|
|
@ -133,7 +143,7 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildButtons() {
|
buildButtons() {
|
||||||
const y = GAME_HEIGHT - 30;
|
const y = 840;
|
||||||
const cx = GAME_WIDTH / 2;
|
const cx = GAME_WIDTH / 2;
|
||||||
this.btn = {};
|
this.btn = {};
|
||||||
this.btn.play = new Button(this, cx - 300, y, 'Play', () => this.humanCommit(),
|
this.btn.play = new Button(this, cx - 300, y, 'Play', () => this.humanCommit(),
|
||||||
|
|
@ -144,7 +154,7 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
{ width: 180, height: 52, fontSize: 22, variant: 'ghost' }).setDepth(D.ui);
|
{ width: 180, height: 52, fontSize: 22, variant: 'ghost' }).setDepth(D.ui);
|
||||||
this.btn.sort = new Button(this, cx + 300, y, 'Sort', () => this.sortRack(),
|
this.btn.sort = new Button(this, cx + 300, y, 'Sort', () => this.sortRack(),
|
||||||
{ width: 180, height: 52, fontSize: 22, variant: 'ghost' }).setDepth(D.ui);
|
{ width: 180, height: 52, fontSize: 22, variant: 'ghost' }).setDepth(D.ui);
|
||||||
this.btn.leave = new Button(this, GAME_WIDTH - 100, 40, 'Leave', () => this.scene.start('GameMenu'),
|
this.btn.leave = new Button(this, GAME_WIDTH - 100, 140, 'Leave', () => this.scene.start('GameMenu'),
|
||||||
{ variant: 'ghost', width: 150, height: 46, fontSize: 19 }).setDepth(D.ui);
|
{ variant: 'ghost', width: 150, height: 46, fontSize: 19 }).setDepth(D.ui);
|
||||||
this.hideActionButtons();
|
this.hideActionButtons();
|
||||||
}
|
}
|
||||||
|
|
@ -278,6 +288,15 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
if (this.isHumanTurn()) this.makeDraggable(obj);
|
if (this.isHumanTurn()) this.makeDraggable(obj);
|
||||||
this.tableObjs.push(obj);
|
this.tableObjs.push(obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Point value label below valid sets containing staged tiles.
|
||||||
|
if (this.isHumanTurn() && valid.valid && set.some((t) => !this.originalTableIds.has(t.id))) {
|
||||||
|
const label = this.add.text(rect.x + rect.w / 2, rect.y + rect.h + 4, `${valid.points} pts`, {
|
||||||
|
fontFamily: 'Righteous', fontSize: '14px', color: HEX(THEME.brassHi),
|
||||||
|
}).setOrigin(0.5, 0).setDepth(D.ui);
|
||||||
|
this.tableObjs.push(label);
|
||||||
|
}
|
||||||
|
|
||||||
cx += setW + setGap;
|
cx += setW + setGap;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -350,12 +369,16 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
obj.setDepth(D.drag);
|
obj.setDepth(D.drag);
|
||||||
this.dragging = obj;
|
this.dragging = obj;
|
||||||
playSound(this, SFX.PIECE_CLICK);
|
playSound(this, SFX.PIECE_CLICK);
|
||||||
|
this.tweens.add({ targets: obj, scaleX: 1.06, scaleY: 1.06, duration: 120, ease: 'Cubic.easeOut' });
|
||||||
|
if (obj._loc?.type === 'rack') this._initRackDragState(obj);
|
||||||
});
|
});
|
||||||
obj.on('drag', (p, dx, dy) => { obj.x = dx; obj.y = dy; this.pointer = p; });
|
obj.on('drag', (p, dx, dy) => { obj.x = dx; obj.y = dy; this.pointer = p; this._onRackDragMove(obj, p); });
|
||||||
obj.on('dragend', (p) => this.onDrop(obj, p));
|
obj.on('dragend', (p) => this.onDrop(obj, p));
|
||||||
}
|
}
|
||||||
|
|
||||||
onDrop(obj, pointer) {
|
onDrop(obj, pointer) {
|
||||||
|
const insertIndex = this.rackDragState?.insertIdx;
|
||||||
|
this._cleanupRackDragState();
|
||||||
this.dragging = null;
|
this.dragging = null;
|
||||||
const px = pointer.x, py = pointer.y;
|
const px = pointer.x, py = pointer.y;
|
||||||
const tile = obj.tileRef;
|
const tile = obj.tileRef;
|
||||||
|
|
@ -372,7 +395,7 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
if (!dest && py >= rack.y - 30) {
|
if (!dest && py >= rack.y - 30) {
|
||||||
if (this.originalTableIds.has(tile.id)) dest = null; // table tiles can't return to rack
|
if (this.originalTableIds.has(tile.id)) dest = null; // table tiles can't return to rack
|
||||||
else dest = { type: 'rack' };
|
else dest = { type: 'rack', insertIndex: insertIndex ?? this.state.workingRack.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dest) {
|
if (dest) {
|
||||||
|
|
@ -382,6 +405,126 @@ export default class RummikubGame extends Phaser.Scene {
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Rack insert-indicator helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
_rackGridPos(slotIdx, totalSlots) {
|
||||||
|
const r = this.layout.rack;
|
||||||
|
const tileW = TW * RACK_SCALE + TILE_GAP;
|
||||||
|
const perRow = Math.max(1, Math.floor(r.w / tileW));
|
||||||
|
const rows = Math.ceil(totalSlots / perRow) || 1;
|
||||||
|
const rowH = TH * RACK_SCALE + 8;
|
||||||
|
const row = Math.floor(slotIdx / perRow);
|
||||||
|
const slotsInRow = Math.min(perRow, totalSlots - row * perRow);
|
||||||
|
const col = slotIdx % perRow;
|
||||||
|
const rowW = slotsInRow * tileW;
|
||||||
|
const x = r.x + (r.w - rowW) / 2 + col * tileW + tileW / 2;
|
||||||
|
const y = r.y + (r.h - rows * rowH) / 2 + row * rowH + rowH / 2;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
_rackInsertIdx(px, py, totalSlots) {
|
||||||
|
let best = 0, bestDist = Infinity;
|
||||||
|
for (let k = 0; k < totalSlots; k++) {
|
||||||
|
const { x, y } = this._rackGridPos(k, totalSlots);
|
||||||
|
const d = (px - x) * (px - x) + (py - y) * (py - y);
|
||||||
|
if (d < bestDist) { bestDist = d; best = k; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeSlotIndicator(x, y) {
|
||||||
|
const g = this.add.graphics();
|
||||||
|
g.fillStyle(THEME.brassHi, 0.15);
|
||||||
|
g.fillRoundedRect(-TW / 2 - 5, -TH / 2 - 5, TW + 10, TH + 10, TR + 2);
|
||||||
|
g.lineStyle(2.5, THEME.brassHi, 0.85);
|
||||||
|
g.strokeRoundedRect(-TW / 2 - 5, -TH / 2 - 5, TW + 10, TH + 10, TR + 2);
|
||||||
|
return g.setPosition(x, y).setDepth(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
_initRackDragState(obj) {
|
||||||
|
const initIdx = this.rackObjs.indexOf(obj);
|
||||||
|
const totalSlots = this.state.workingRack.length;
|
||||||
|
const pos = this._rackGridPos(Math.max(0, initIdx), totalSlots);
|
||||||
|
const indicator = this._makeSlotIndicator(pos.x, pos.y);
|
||||||
|
this.rackDragState = { insertIdx: initIdx, slotIndicator: indicator, isRackTile: true, visible: true };
|
||||||
|
this._updateRackGap(obj, totalSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRackDragMove(obj, pointer) {
|
||||||
|
const rack = this.layout.rack;
|
||||||
|
const px = pointer.x, py = pointer.y;
|
||||||
|
const inRackZone = py >= rack.y - 40;
|
||||||
|
const isRackTile = obj._loc?.type === 'rack';
|
||||||
|
|
||||||
|
if (!inRackZone) {
|
||||||
|
if (this.rackDragState?.visible) {
|
||||||
|
this.rackDragState.slotIndicator.setAlpha(0);
|
||||||
|
this.rackDragState.visible = false;
|
||||||
|
this._settleRackTiles(obj);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSlots = isRackTile
|
||||||
|
? this.state.workingRack.length
|
||||||
|
: this.state.workingRack.length + 1;
|
||||||
|
|
||||||
|
if (!this.rackDragState) {
|
||||||
|
const initIdx = isRackTile ? this.rackObjs.indexOf(obj) : this.state.workingRack.length;
|
||||||
|
const clamped = Math.max(0, Math.min(initIdx, totalSlots - 1));
|
||||||
|
const pos = this._rackGridPos(clamped, totalSlots);
|
||||||
|
this.rackDragState = {
|
||||||
|
insertIdx: clamped,
|
||||||
|
slotIndicator: this._makeSlotIndicator(pos.x, pos.y),
|
||||||
|
isRackTile,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ds = this.rackDragState;
|
||||||
|
const wasHidden = !ds.visible;
|
||||||
|
ds.visible = true;
|
||||||
|
ds.slotIndicator.setAlpha(1);
|
||||||
|
|
||||||
|
const newInsertIdx = this._rackInsertIdx(px, py, totalSlots);
|
||||||
|
if (newInsertIdx !== ds.insertIdx || wasHidden) {
|
||||||
|
ds.insertIdx = newInsertIdx;
|
||||||
|
const pos = this._rackGridPos(newInsertIdx, totalSlots);
|
||||||
|
this.tweens.killTweensOf(ds.slotIndicator);
|
||||||
|
this.tweens.add({ targets: ds.slotIndicator, x: pos.x, y: pos.y, duration: 100, ease: 'Cubic.easeOut' });
|
||||||
|
this._updateRackGap(obj, totalSlots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateRackGap(draggedObj, totalSlots) {
|
||||||
|
const insertIdx = this.rackDragState.insertIdx;
|
||||||
|
let k = 0;
|
||||||
|
for (const rackObj of this.rackObjs) {
|
||||||
|
if (rackObj === draggedObj) continue;
|
||||||
|
const targetSlot = k < insertIdx ? k : k + 1;
|
||||||
|
const pos = this._rackGridPos(targetSlot, totalSlots);
|
||||||
|
this.tweens.killTweensOf(rackObj);
|
||||||
|
this.tweens.add({ targets: rackObj, x: pos.x, y: pos.y, duration: 100, ease: 'Cubic.easeOut' });
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_settleRackTiles(draggedObj) {
|
||||||
|
const others = this.rackObjs.filter((o) => o !== draggedObj);
|
||||||
|
others.forEach((rackObj, k) => {
|
||||||
|
const pos = this._rackGridPos(k, others.length);
|
||||||
|
this.tweens.killTweensOf(rackObj);
|
||||||
|
this.tweens.add({ targets: rackObj, x: pos.x, y: pos.y, duration: 100, ease: 'Cubic.easeOut' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_cleanupRackDragState() {
|
||||||
|
if (this.rackDragState) {
|
||||||
|
this.rackDragState.slotIndicator.destroy();
|
||||||
|
this.rackDragState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Human actions ─────────────────────────────────────────────────────────---
|
// ── Human actions ─────────────────────────────────────────────────────────---
|
||||||
humanCommit() {
|
humanCommit() {
|
||||||
if (!this.isHumanTurn()) return;
|
if (!this.isHumanTurn()) return;
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,9 @@ export function applyDrag(state, tileId, dest) {
|
||||||
if (!tile) return state; // unknown tile
|
if (!tile) return state; // unknown tile
|
||||||
|
|
||||||
if (dest.type === 'rack') {
|
if (dest.type === 'rack') {
|
||||||
workingRack.push(tile);
|
const idx = (dest.insertIndex !== undefined && dest.insertIndex >= 0)
|
||||||
|
? Math.min(dest.insertIndex, workingRack.length) : workingRack.length;
|
||||||
|
workingRack.splice(idx, 0, tile);
|
||||||
} else if (dest.type === 'newSet') {
|
} else if (dest.type === 'newSet') {
|
||||||
workingTable.push([tile]);
|
workingTable.push([tile]);
|
||||||
} else if (dest.type === 'set') {
|
} else if (dest.type === 'set') {
|
||||||
|
|
@ -133,10 +135,21 @@ export function stageAiPlan(state, plan) {
|
||||||
|
|
||||||
export function resetTurn(state) {
|
export function resetTurn(state) {
|
||||||
const ts = state.turnStart;
|
const ts = state.turnStart;
|
||||||
|
// Preserve manual rack ordering: tiles already in workingRack keep their
|
||||||
|
// current positions; tiles being returned from the table append at the end
|
||||||
|
// in their original turnStart order.
|
||||||
|
const rankInWorking = new Map(state.workingRack.map((t, i) => [t.id, i]));
|
||||||
|
const origRank = new Map(ts.rack.map((t, i) => [t.id, i]));
|
||||||
|
const n = ts.rack.length;
|
||||||
|
const resetRack = ts.rack.slice().sort((a, b) => {
|
||||||
|
const ai = rankInWorking.has(a.id) ? rankInWorking.get(a.id) : n + origRank.get(a.id);
|
||||||
|
const bi = rankInWorking.has(b.id) ? rankInWorking.get(b.id) : n + origRank.get(b.id);
|
||||||
|
return ai - bi;
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
workingTable: ts.table.map((s) => s.slice()),
|
workingTable: ts.table.map((s) => s.slice()),
|
||||||
workingRack: ts.rack.slice(),
|
workingRack: resetRack,
|
||||||
drewThisTurn: false,
|
drewThisTurn: false,
|
||||||
commitError: null,
|
commitError: null,
|
||||||
};
|
};
|
||||||
|
|
@ -230,8 +243,11 @@ export function drawTile(state) {
|
||||||
|
|
||||||
const pool = state.pool.slice();
|
const pool = state.pool.slice();
|
||||||
const tile = pool.shift();
|
const tile = pool.shift();
|
||||||
|
// Use workingRack so any manual reordering done before drawing is preserved;
|
||||||
|
// draw is only allowed when no tiles have been moved to the table, so
|
||||||
|
// workingRack has the same tile set as players[seat].rack.
|
||||||
const players = state.players.map((p, i) =>
|
const players = state.players.map((p, i) =>
|
||||||
i === seat ? { ...p, rack: [...state.players[seat].rack, tile] } : p);
|
i === seat ? { ...p, rack: [...state.workingRack, tile] } : p);
|
||||||
const next = { ...state, pool, players, consecutivePasses: 0, drewThisTurn: true, commitError: null };
|
const next = { ...state, pool, players, consecutivePasses: 0, drewThisTurn: true, commitError: null };
|
||||||
return advanceTurn(next);
|
return advanceTurn(next);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue