Two treasures, one square: the taker names their prize
pickUpTreasure grabbed whichever treasure the list offered first — with two on a square, no choice and sometimes the wrong one. The command now takes an optional treasureId (absent in every stored ledger, so old replays keep their old grab), and when the square holds more than one, the button asks "whose?" before dispatching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
794b809a1b
commit
aef88d892a
@@ -660,7 +660,7 @@ export type Command =
|
|||||||
| { type: "cancelAmbush"; ambushId: string }
|
| { type: "cancelAmbush"; ambushId: string }
|
||||||
| { type: "counteract"; instanceId: string; params?: { cell?: Cell }; numberInstanceIds?: string[] }
|
| { type: "counteract"; instanceId: string; params?: { cell?: Cell }; numberInstanceIds?: string[] }
|
||||||
| { type: "pass" }
|
| { type: "pass" }
|
||||||
| { type: "pickUpTreasure" }
|
| { type: "pickUpTreasure"; treasureId?: string }
|
||||||
| { type: "pickUpObject"; instanceId: string }
|
| { type: "pickUpObject"; instanceId: string }
|
||||||
| { type: "dropObject"; instanceId: string }
|
| { type: "dropObject"; instanceId: string }
|
||||||
| { type: "dropTreasure" }
|
| { type: "dropTreasure" }
|
||||||
@@ -3380,7 +3380,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman
|
|||||||
case "cancelAmbush": return doCancelAmbush(state, command.ambushId);
|
case "cancelAmbush": return doCancelAmbush(state, command.ambushId);
|
||||||
case "counteract": return err("nothing to counteract");
|
case "counteract": return err("nothing to counteract");
|
||||||
case "pass": return err("nothing to pass on");
|
case "pass": return err("nothing to pass on");
|
||||||
case "pickUpTreasure": return doPickUpTreasure(state);
|
case "pickUpTreasure": return doPickUpTreasure(state, command.treasureId);
|
||||||
case "pickUpObject": return doPickUpObject(state, command.instanceId);
|
case "pickUpObject": return doPickUpObject(state, command.instanceId);
|
||||||
case "dropObject": return doDropObject(state, command.instanceId);
|
case "dropObject": return doDropObject(state, command.instanceId);
|
||||||
case "dropTreasure": return doDropTreasure(state);
|
case "dropTreasure": return doDropTreasure(state);
|
||||||
@@ -5248,7 +5248,7 @@ function homeOwnerAt(state: GameState, cell: Cell): PlayerId | null {
|
|||||||
|
|
||||||
// --- Treasures ---------------------------------------------------------------
|
// --- Treasures ---------------------------------------------------------------
|
||||||
|
|
||||||
function doPickUpTreasure(prev: GameState): CommandResult {
|
function doPickUpTreasure(prev: GameState, treasureId?: string): CommandResult {
|
||||||
const blocked = requireActionsAvailable(prev);
|
const blocked = requireActionsAvailable(prev);
|
||||||
if (blocked) return err(blocked);
|
if (blocked) return err(blocked);
|
||||||
|
|
||||||
@@ -5262,10 +5262,12 @@ function doPickUpTreasure(prev: GameState): CommandResult {
|
|||||||
if (safe && state.squareContents[here]!.createdBy !== p.id && !state.openSafes.includes(here)) {
|
if (safe && state.squareContents[here]!.createdBy !== p.id && !state.openSafes.includes(here)) {
|
||||||
return err("it is locked inside a safe");
|
return err("it is locked inside a safe");
|
||||||
}
|
}
|
||||||
|
// Two treasures can share a square; an id names the one wanted.
|
||||||
const t = state.treasures.find(
|
const t = state.treasures.find(
|
||||||
(t) => t.position && cellKey(t.position) === cellKey(p.position) && !t.carriedBy,
|
(t) => t.position && cellKey(t.position) === cellKey(p.position) && !t.carriedBy &&
|
||||||
|
(treasureId == null || t.id === treasureId),
|
||||||
);
|
);
|
||||||
if (!t) return err("no treasure here");
|
if (!t) return err(treasureId ? "that treasure is not here for the taking" : "no treasure here");
|
||||||
|
|
||||||
t.carriedBy = p.id;
|
t.carriedBy = p.id;
|
||||||
t.position = null;
|
t.position = null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { applyCommand, activePlayer, boardView, gameLos, sustainedOn } from "../src/game";
|
import { applyCommand, activePlayer, boardView, createGame, gameLos, sustainedOn } from "../src/game";
|
||||||
import { cellKey, edgeKey, neighbor, SIDES } from "../src/board";
|
import { cellKey, edgeKey, neighbor, SIDES } from "../src/board";
|
||||||
import type { CardInstance } from "../src/cards";
|
import type { CardInstance } from "../src/cards";
|
||||||
import { newGame, must, giveCard, toRound2, faceOff, castAt, emptyNeighborCell } from "./helpers";
|
import { newGame, must, giveCard, toRound2, faceOff, castAt, emptyNeighborCell } from "./helpers";
|
||||||
@@ -277,3 +277,17 @@ describe("control effects", () => {
|
|||||||
expect(state.players.find((p) => p.id === me.id)!.hand.some((c) => c.cardId === "create-wall")).toBe(true);
|
expect(state.players.find((p) => p.id === me.id)!.hand.some((c) => c.cardId === "create-wall")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("picking one of two treasures", () => {
|
||||||
|
it("a named treasure is the one taken", () => {
|
||||||
|
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev: 16 });
|
||||||
|
state = toRound2(state);
|
||||||
|
const p = activePlayer(state);
|
||||||
|
const [t1, t2] = state.treasures.filter((t) => t.owner !== p.id);
|
||||||
|
t1!.position = { ...p.position }; t1!.carriedBy = null;
|
||||||
|
t2!.position = { ...p.position }; t2!.carriedBy = null;
|
||||||
|
const r = applyCommand(state, p.id, { type: "pickUpTreasure", treasureId: t2!.id });
|
||||||
|
if (!r.ok) throw new Error(r.error);
|
||||||
|
expect(r.state.players.find((q) => q.id === p.id)!.carriedTreasureId).toBe(t2!.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -313,6 +313,7 @@
|
|||||||
selectedCard = null;
|
selectedCard = null;
|
||||||
megaBoost = "life";
|
megaBoost = "life";
|
||||||
holdDoor = false;
|
holdDoor = false;
|
||||||
|
pickChoice = false;
|
||||||
pendingTeleport = null;
|
pendingTeleport = null;
|
||||||
attachedNumber = null;
|
attachedNumber = null;
|
||||||
attachedMods = [];
|
attachedMods = [];
|
||||||
@@ -821,11 +822,26 @@
|
|||||||
dispatch({ type: "endTurn", draw: drawCount });
|
dispatch({ type: "endTurn", draw: drawCount });
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickUp() { dispatch({ type: "pickUpTreasure" }); }
|
let pickChoice = $state(false);
|
||||||
|
function pickUp() {
|
||||||
|
if (treasuresHere.length > 1) { pickChoice = true; return; }
|
||||||
|
dispatch({ type: "pickUpTreasure" });
|
||||||
|
}
|
||||||
|
function pickUpNamed(treasureId: string) {
|
||||||
|
pickChoice = false;
|
||||||
|
dispatch({ type: "pickUpTreasure", treasureId });
|
||||||
|
}
|
||||||
function drop() { dispatch({ type: "dropTreasure" }); }
|
function drop() { dispatch({ type: "dropTreasure" }); }
|
||||||
function pass() { dispatch({ type: "pass" }); }
|
function pass() { dispatch({ type: "pass" }); }
|
||||||
|
|
||||||
const me = $derived(view?.players.find((p) => p.id === view?.you) ?? null);
|
const me = $derived(view?.players.find((p) => p.id === view?.you) ?? null);
|
||||||
|
/** Two treasures can share a square: one dispatches, more ask whose. */
|
||||||
|
const treasuresHere = $derived(
|
||||||
|
view && me
|
||||||
|
? view.treasures.filter((t) => t.position && t.position.x === me.position.x &&
|
||||||
|
t.position.y === me.position.y && !t.carriedBy)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
const carryingTreasure = $derived(me?.carriedTreasureId != null);
|
const carryingTreasure = $derived(me?.carriedTreasureId != null);
|
||||||
const treasureHere = $derived(
|
const treasureHere = $derived(
|
||||||
view != null && me != null &&
|
view != null && me != null &&
|
||||||
@@ -1659,6 +1675,15 @@
|
|||||||
{#if isYourTurn}
|
{#if isYourTurn}
|
||||||
<button class="stamp" disabled={!treasureHere || carryingTreasure || view.turn.actionsEnded}
|
<button class="stamp" disabled={!treasureHere || carryingTreasure || view.turn.actionsEnded}
|
||||||
onclick={pickUp}>Pick up treasure</button>
|
onclick={pickUp}>Pick up treasure</button>
|
||||||
|
{#if pickChoice}
|
||||||
|
<span class="pick-which">
|
||||||
|
— whose?
|
||||||
|
{#each treasuresHere as t (t.id)}
|
||||||
|
<button class="stamp tiny" onclick={() => pickUpNamed(t.id)}>{t.owner}'s</button>
|
||||||
|
{/each}
|
||||||
|
<button class="hint-cancel" onclick={() => (pickChoice = false)}>never mind</button>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<button class="stamp" disabled={!carryingTreasure} onclick={drop}>Drop treasure</button>
|
<button class="stamp" disabled={!carryingTreasure} onclick={drop}>Drop treasure</button>
|
||||||
{#each objectsHere as obj (obj.instanceId)}
|
{#each objectsHere as obj (obj.instanceId)}
|
||||||
<button class="stamp" disabled={view.turn.actionsEnded}
|
<button class="stamp" disabled={view.turn.actionsEnded}
|
||||||
|
|||||||
Reference in New Issue
Block a user