Show placed cards before drawing
This commit is contained in:
+12
-4
@@ -9,6 +9,7 @@ import {
|
|||||||
encodeAction,
|
encodeAction,
|
||||||
legalActionMask,
|
legalActionMask,
|
||||||
newGame,
|
newGame,
|
||||||
|
previewPlacement,
|
||||||
step,
|
step,
|
||||||
} from "./game/engine";
|
} from "./game/engine";
|
||||||
import { DISCARD, DRAW_DECK, N_CARDS, PLAY, type GameState, type PlaceType } from "./game/types";
|
import { DISCARD, DRAW_DECK, N_CARDS, PLAY, type GameState, type PlaceType } from "./game/types";
|
||||||
@@ -27,10 +28,18 @@ function App() {
|
|||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const generation = useRef(0);
|
const generation = useRef(0);
|
||||||
|
|
||||||
const scores = useMemo(() => boardScore(state), [state]);
|
|
||||||
const humanHand = useMemo(() => currentHandSorted(state, 0), [state]);
|
const humanHand = useMemo(() => currentHandSorted(state, 0), [state]);
|
||||||
const opponentHand = useMemo(() => currentHandSorted(state, 1), [state]);
|
const opponentHand = useMemo(() => currentHandSorted(state, 1), [state]);
|
||||||
const legal = useMemo(() => legalActionMask(state), [state]);
|
const legal = useMemo(() => legalActionMask(state), [state]);
|
||||||
|
const selectedCard = selection.handSlot === null ? null : humanHand[selection.handSlot];
|
||||||
|
const selectedColor = selectedCard === null ? null : cardColor(selectedCard);
|
||||||
|
const displayState = useMemo(
|
||||||
|
() => selection.handSlot === null || selection.placeType === null
|
||||||
|
? state
|
||||||
|
: previewPlacement(state, selection.handSlot, selection.placeType),
|
||||||
|
[selection, state],
|
||||||
|
);
|
||||||
|
const scores = useMemo(() => boardScore(displayState), [displayState]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -106,8 +115,6 @@ function App() {
|
|||||||
setSelection(EMPTY_SELECTION);
|
setSelection(EMPTY_SELECTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCard = selection.handSlot === null ? null : humanHand[selection.handSlot];
|
|
||||||
const selectedColor = selectedCard === null ? null : cardColor(selectedCard);
|
|
||||||
const canPlaySelected = selection.handSlot !== null && Array.from(
|
const canPlaySelected = selection.handSlot !== null && Array.from(
|
||||||
{ length: 6 },
|
{ length: 6 },
|
||||||
(_, draw) => legal[encodeAction(selection.handSlot!, PLAY, draw)],
|
(_, draw) => legal[encodeAction(selection.handSlot!, PLAY, draw)],
|
||||||
@@ -142,7 +149,7 @@ function App() {
|
|||||||
|
|
||||||
<div className="table-center">
|
<div className="table-center">
|
||||||
<Board
|
<Board
|
||||||
state={state}
|
state={displayState}
|
||||||
selectedColor={selectedColor}
|
selectedColor={selectedColor}
|
||||||
selectedPlace={selection.placeType}
|
selectedPlace={selection.placeType}
|
||||||
canPlay={canPlaySelected}
|
canPlay={canPlaySelected}
|
||||||
@@ -168,6 +175,7 @@ function App() {
|
|||||||
|
|
||||||
<section className="human-hand" aria-label="Your hand">
|
<section className="human-hand" aria-label="Your hand">
|
||||||
{humanHand.map((card, index) => (
|
{humanHand.map((card, index) => (
|
||||||
|
selection.placeType !== null && selection.handSlot === index ? null :
|
||||||
<Card
|
<Card
|
||||||
card={card}
|
card={card}
|
||||||
key={card}
|
key={card}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { boardScore, currentHandSorted, encodeAction, legalActionMask, resetFromOrder, step } from "./engine";
|
import {
|
||||||
import { DISCARD, DRAW_DECK, N_CARDS } from "./types";
|
boardScore,
|
||||||
|
cardsOnBoard,
|
||||||
|
currentHandSorted,
|
||||||
|
encodeAction,
|
||||||
|
legalActionMask,
|
||||||
|
previewPlacement,
|
||||||
|
resetFromOrder,
|
||||||
|
step,
|
||||||
|
} from "./engine";
|
||||||
|
import { DISCARD, DRAW_DECK, N_CARDS, PLAY } from "./types";
|
||||||
import { cardColor } from "./cards";
|
import { cardColor } from "./cards";
|
||||||
import { observation } from "./observation";
|
import { observation } from "./observation";
|
||||||
import fixture from "./parity-fixture.json";
|
import fixture from "./parity-fixture.json";
|
||||||
@@ -24,6 +33,19 @@ describe("JAX-compatible game engine", () => {
|
|||||||
expect(mask[encodeAction(0, DISCARD, DRAW_DECK)]).toBe(true);
|
expect(mask[encodeAction(0, DISCARD, DRAW_DECK)]).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("previews a placed card without ending the turn", () => {
|
||||||
|
const state = resetFromOrder(orderedDeck);
|
||||||
|
const card = currentHandSorted(state)[3];
|
||||||
|
const preview = previewPlacement(state, 3, PLAY);
|
||||||
|
|
||||||
|
expect(currentHandSorted(preview, 0)).not.toContain(card);
|
||||||
|
expect(cardsOnBoard(preview, 0, cardColor(card))).toContain(card);
|
||||||
|
expect(preview.toMove).toBe(0);
|
||||||
|
expect(preview.drawPtr).toBe(state.drawPtr);
|
||||||
|
expect(preview.stepCount).toBe(state.stepCount);
|
||||||
|
expect(currentHandSorted(state, 0)).toContain(card);
|
||||||
|
});
|
||||||
|
|
||||||
test("deck-only games terminate after exactly 44 plies", () => {
|
test("deck-only games terminate after exactly 44 plies", () => {
|
||||||
let state = resetFromOrder(orderedDeck);
|
let state = resetFromOrder(orderedDeck);
|
||||||
let plies = 0;
|
let plies = 0;
|
||||||
|
|||||||
+28
-11
@@ -114,18 +114,8 @@ export function legalActionMask(state: GameState): boolean[] {
|
|||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function step(state: GameState, action: number): GameState {
|
function placeCard(next: GameState, player: number, card: number, placeType: PlaceType): void {
|
||||||
const mask = legalActionMask(state);
|
|
||||||
if (!Number.isInteger(action) || action < 0 || action >= N_ACTIONS || !mask[action]) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
const next = cloneState(state);
|
|
||||||
const { handSlot, placeType, drawSource } = decodeAction(action);
|
|
||||||
const player = state.toMove;
|
|
||||||
const hand = currentHandSorted(state, player);
|
|
||||||
const card = hand[handSlot];
|
|
||||||
const color = cardColor(card);
|
const color = cardColor(card);
|
||||||
|
|
||||||
next.handPublic[card] = false;
|
next.handPublic[card] = false;
|
||||||
if (placeType === PLAY) {
|
if (placeType === PLAY) {
|
||||||
next.cardLoc[card] = LOC_P0_BOARD + player;
|
next.cardLoc[card] = LOC_P0_BOARD + player;
|
||||||
@@ -136,6 +126,33 @@ export function step(state: GameState, action: number): GameState {
|
|||||||
next.cardLoc[card] = LOC_DISCARD;
|
next.cardLoc[card] = LOC_DISCARD;
|
||||||
next.piles[color].push(card);
|
next.piles[color].push(card);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previewPlacement(state: GameState, handSlot: number, placeType: PlaceType): GameState {
|
||||||
|
const mask = legalActionMask(state);
|
||||||
|
const hasLegalDraw = Array.from(
|
||||||
|
{ length: N_COLORS + 1 },
|
||||||
|
(_, drawSource) => mask[encodeAction(handSlot, placeType, drawSource)],
|
||||||
|
).some(Boolean);
|
||||||
|
if (!hasLegalDraw) return state;
|
||||||
|
|
||||||
|
const next = cloneState(state);
|
||||||
|
const card = currentHandSorted(state)[handSlot];
|
||||||
|
placeCard(next, state.toMove, card, placeType);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function step(state: GameState, action: number): GameState {
|
||||||
|
const mask = legalActionMask(state);
|
||||||
|
if (!Number.isInteger(action) || action < 0 || action >= N_ACTIONS || !mask[action]) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
const next = cloneState(state);
|
||||||
|
const { handSlot, placeType, drawSource } = decodeAction(action);
|
||||||
|
const player = state.toMove;
|
||||||
|
const hand = currentHandSorted(state, player);
|
||||||
|
const card = hand[handSlot];
|
||||||
|
placeCard(next, player, card, placeType);
|
||||||
|
|
||||||
let drawnCard: number;
|
let drawnCard: number;
|
||||||
if (drawSource === DRAW_DECK) {
|
if (drawSource === DRAW_DECK) {
|
||||||
|
|||||||
Reference in New Issue
Block a user