Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/lib/challengeStartPosition.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getMainLineTipState } from "./Lab/exploration";
import { getFocusNode } from "./Lab/exploration";
import { isTeachingChallengeEligible } from "./teachingModeChallenge";

/** Mirrors backend teaching eligibility for start-position challenges. */
Expand Down Expand Up @@ -28,7 +28,7 @@ export function buildStartPositionFromLabSession({ game, nodes, focus }) {
if (!game?.metaGame || !nodes?.length || !focus) {
throw new Error("Cannot build start position from this session.");
}
const state = getMainLineTipState(nodes, game);
const state = getFocusNode(nodes, game, focus)?.state;
if (!state) {
throw new Error("Start position state is missing.");
}
Expand Down
94 changes: 93 additions & 1 deletion src/lib/challengeStartPosition.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GameFactory } from "@abstractplay/gameslib";
import {
buildStartPositionPayload,
buildStartPositionFromLabSession,
buildStartPositionFromGameMoveSession,
isStartPositionChallengeEligible,
} from "./challengeStartPosition";

Expand All @@ -19,7 +20,7 @@ describe("challengeStartPosition", () => {
expect(isStartPositionChallengeEligible("saltire", 3, [])).toBe(false);
});

it("buildStartPositionFromLabSession uses tip state at focus", () => {
it("buildStartPositionFromLabSession uses the selected main-line tip", () => {
const engine = GameFactory("saltire", undefined, []);
const opening = engine.serialize();
engine.move(engine.moves()[0]);
Expand All @@ -39,3 +40,94 @@ describe("challengeStartPosition", () => {
expect(draft.playerCount).toBe(2);
});
});

describe.each([
["Lab", buildStartPositionFromLabSession],
["Game Move", buildStartPositionFromGameMoveSession],
])("%s start-position selection", (_name, buildStartPosition) => {
it("uses an earlier main-line position, hydrating its snapshot", () => {
const engine = GameFactory("saltire", undefined, []);
engine.move(engine.moves()[0]);
const selected = engine.serialize();
engine.move(engine.moves()[0]);
const tip = engine.serialize();
const nodes = [
{ state: null, children: [] },
{ state: null, children: [] },
{ state: tip, children: [] },
];
const draft = buildStartPosition({
game: { metaGame: "saltire", state: tip, selectedVariants: [] },
nodes,
focus: { moveNumber: 1, exPath: [] },
});
const actual = GameFactory("saltire", draft.state);
const expected = GameFactory("saltire", selected);
expect(actual.stack).toHaveLength(2);
expect(actual.board).toEqual(expected.board);
expect(actual.currplayer).toBe(expected.currplayer);
});

it("uses the nested Arimaa exploration branch from the selected focus", () => {
const main = GameFactory("arimaa", undefined, ["free"]);
const opening = main.serialize();
main.move("Re1, Ee2, Md2, Hc2, Hb2, Da2, Da1, Cc1, Cb1");
const mainGold = main.serialize();
main.move(
"ra8, rb8, rc8, rd8, re8, rf8, rg8, rh8, rh7, rg7, rf7, re7, rd7, rc7, rb7, ra7, ra6, rb6, rc6, rd6, re6, rf6, rg6, rh6, rh5, rg5, rf5, re5, rd5, rc5, rb5, ra5"
);
const tip = main.serialize();

const branch = GameFactory("arimaa", opening);
branch.move(
"Ee2, Hb2, Dg2, Cd2, Ra2, Ra1, Rb1, Rc1, Rc2, Rd1, Re1, Rf1, Rf2, Rh1, Rg1, Rh2"
);
const branchGold = branch.serialize();
branch.move(
"ed7, me7, hg7, hb7, dd8, de8, cf7, cc7, ra7, ra8, rb8, rc8, rf8, rg8, rh8, rh7"
);
const selected = branch.serialize();
const siblings = Array.from({ length: 6 }, () => ({
state: opening,
children: [],
}));
const nodes = [
{
state: opening,
children: [
...siblings,
{
state: branchGold,
children: [{ state: selected, children: [] }],
},
],
},
{ state: mainGold, children: [] },
{ state: tip, children: [] },
];
const draft = buildStartPosition({
game: { metaGame: "arimaa", state: tip, selectedVariants: ["free"] },
nodes,
focus: { moveNumber: 0, exPath: [6, 0] },
});

expect(draft.state).toBe(selected);
expect(buildStartPositionPayload(draft.state)).toEqual({
startPosition: { state: selected },
});
expect(draft.variants).toEqual(["free"]);
const actual = GameFactory("arimaa", draft.state);
expect(actual.board.get("d7")).toEqual(["E", 2]);
expect(actual.board.size).toBe(32);
});

it("rejects a missing focus node instead of using the main-line tip", () => {
expect(() =>
buildStartPosition({
game: { metaGame: "saltire" },
nodes: [{ state: "tip", children: [] }],
focus: { moveNumber: 0, exPath: [0] },
})
).toThrow("Start position state is missing.");
});
});