Skip to content

Commit

Permalink
Refactor passing state into findSet (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
eltoder authored Feb 8, 2025
1 parent b95f543 commit c508e68
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 77 deletions.
43 changes: 22 additions & 21 deletions functions/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ interface GameModeInfo {
chain: number;
}

export interface FindState {
lastSet?: string[];
}

/** Generates a random seed. */
export function generateSeed() {
let s = "v1:";
Expand Down Expand Up @@ -94,9 +98,10 @@ export function checkSetGhost(
return [a, b, c, d, e, f];
}

function findSetNormal(deck: string[], gameMode: GameMode, old?: string[]) {
function findSetNormal(deck: string[], gameMode: GameMode, state: FindState) {
const deckSet = new Set(deck);
const first = modes[gameMode].chain && old!.length > 0 ? old! : deck;
const first =
modes[gameMode].chain && state.lastSet!.length > 0 ? state.lastSet! : deck;
for (let i = 0; i < first.length; i++) {
for (let j = first === deck ? i + 1 : 0; j < deck.length; j++) {
const c = conjugateCard(first[i], deck[j]);
Expand All @@ -108,11 +113,11 @@ function findSetNormal(deck: string[], gameMode: GameMode, old?: string[]) {
return null;
}

function findSetUltra(deck: string[], gameMode: GameMode, old?: string[]) {
const cutoff = modes[gameMode].chain ? old!.length : 0;
function findSetUltra(deck: string[], gameMode: GameMode, state: FindState) {
const cutoff = modes[gameMode].chain ? state.lastSet!.length : 0;
let cards, conjugates: Array<Map<string, [string, string]> | null>;
if (cutoff > 0) {
cards = old!.concat(deck);
cards = state.lastSet!.concat(deck);
conjugates = [new Map(), null, null, null].fill(new Map(), 1, 3);
} else {
cards = deck;
Expand All @@ -134,7 +139,7 @@ function findSetUltra(deck: string[], gameMode: GameMode, old?: string[]) {
return null;
}

function findSetGhost(deck: string[], gameMode: GameMode, old?: string[]) {
function findSetGhost(deck: string[], gameMode: GameMode, state: FindState) {
for (let i = 0; i < deck.length; i++) {
for (let j = i + 1; j < deck.length; j++) {
for (let k = j + 1; k < deck.length; k++) {
Expand All @@ -160,8 +165,8 @@ function findSetGhost(deck: string[], gameMode: GameMode, old?: string[]) {
}

/** Find a set in an unordered collection of cards, if any, depending on mode. */
export function findSet(deck: string[], gameMode: GameMode, old?: string[]) {
return setTypes[modes[gameMode].setType].findFn(deck, gameMode, old);
export function findSet(deck: string[], gameMode: GameMode, state: FindState) {
return setTypes[modes[gameMode].setType].findFn(deck, gameMode, state);
}

/** Get the array of cards from a GameEvent */
Expand Down Expand Up @@ -198,18 +203,20 @@ function replayEvent(
deck: Set<string>,
event: GameEvent,
chain: number,
history: GameEvent[]
findState: FindState
) {
const allCards = cardsFromEvent(event);
let cards;
if (chain && history.length > 0) {
const prev = cardsFromEvent(history[history.length - 1]);
cards = allCards.filter((c) => !prev.includes(c));
if (chain && findState.lastSet!.length > 0) {
cards = allCards.filter((c) => !findState.lastSet!.includes(c));
if (allCards.length - cards.length !== chain) return false;
} else {
cards = allCards;
}
if (hasDuplicates(allCards) || !validCards(deck, cards)) return false;
if (chain) {
findState.lastSet = allCards;
}
deleteCards(deck, cards);
return true;
}
Expand Down Expand Up @@ -300,21 +307,15 @@ export function replayEvents(

const deck = generateDeck(gameMode);
const chain = modes[gameMode].chain;
const history: GameEvent[] = [];
const findState: FindState = { lastSet: chain ? [] : undefined };
const scores: Record<string, number> = {};
let finalTime = 0;
for (const event of events) {
if (replayEvent(deck, event, chain, history)) {
history.push(event);
if (replayEvent(deck, event, chain, findState)) {
scores[event.user] = (scores[event.user] ?? 0) + 1;
finalTime = event.time;
}
}

const lastSet =
chain && history.length > 0
? cardsFromEvent(history[history.length - 1])
: [];

return { lastSet, deck, finalTime, scores };
return { findState, deck, finalTime, scores };
}
7 changes: 5 additions & 2 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ export const finishGame = functions.https.onCall(async (data, context) => {
}

const gameMode = (gameSnap.child("mode").val() as GameMode) || "normal";
const { lastSet, deck, finalTime, scores } = replayEvents(gameData, gameMode);
const { findState, deck, finalTime, scores } = replayEvents(
gameData,
gameMode
);

if (findSet(Array.from(deck), gameMode, lastSet)) {
if (findSet(Array.from(deck), gameMode, findState)) {
throw new functions.https.HttpsError(
"failed-precondition",
"The requested game has not yet ended."
Expand Down
68 changes: 37 additions & 31 deletions src/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,8 @@ function makeCards(symbols, traits) {
);
}

export function generateCards(gameMode) {
return makeCards(["0", "1", "2"], modes[gameMode].traits);
}

export function generateDeck(gameMode, seed) {
const deck = generateCards(gameMode);
const deck = makeCards(["0", "1", "2"], modes[gameMode].traits);
// Fisher-Yates
const random = makeRandom(seed);
for (let i = deck.length - 1; i > 0; i--) {
Expand Down Expand Up @@ -94,10 +90,11 @@ export function checkSetGhost(a, b, c, d, e, f) {
return [a, b, c, d, e, f];
}

export function addCard(deck, card, gameMode, lastSet) {
export function addCard(deck, card, gameMode, findState) {
const setType = modes[gameMode].setType;
const chain = modes[gameMode].chain;
const setSize = setTypes[setType].size;
const { lastSet } = findState;
// Special case for the regular set-chain modes: if you select a second card
// from lastSet, we unselect the first one
const cards =
Expand Down Expand Up @@ -153,9 +150,10 @@ export function cardTraits(card) {
};
}

function findSetNormal(deck, gameMode, old) {
function findSetNormal(deck, gameMode, state) {
const deckSet = new Set(deck);
const first = modes[gameMode].chain && old.length > 0 ? old : deck;
const first =
modes[gameMode].chain && state.lastSet.length > 0 ? state.lastSet : deck;
for (let i = 0; i < first.length; i++) {
for (let j = first === deck ? i + 1 : 0; j < deck.length; j++) {
const c = conjugateCard(first[i], deck[j]);
Expand All @@ -167,11 +165,11 @@ function findSetNormal(deck, gameMode, old) {
return null;
}

function findSetUltra(deck, gameMode, old) {
const cutoff = modes[gameMode].chain ? old.length : 0;
function findSetUltra(deck, gameMode, state) {
const cutoff = modes[gameMode].chain ? state.lastSet.length : 0;
let cards, conjugates;
if (cutoff > 0) {
cards = old.concat(deck);
cards = state.lastSet.concat(deck);
conjugates = [new Map(), null, null, null].fill(new Map(), 1, 3);
} else {
cards = deck;
Expand All @@ -193,7 +191,7 @@ function findSetUltra(deck, gameMode, old) {
return null;
}

function findSetGhost(deck, gameMode, old) {
function findSetGhost(deck, gameMode, state) {
for (let i = 0; i < deck.length; i++) {
for (let j = i + 1; j < deck.length; j++) {
for (let k = j + 1; k < deck.length; k++) {
Expand All @@ -218,8 +216,8 @@ function findSetGhost(deck, gameMode, old) {
return null;
}

export function findSet(deck, gameMode, old) {
return setTypes[modes[gameMode].setType].findFn(deck, gameMode, old);
export function findSet(deck, gameMode, state) {
return setTypes[modes[gameMode].setType].findFn(deck, gameMode, state);
}

export function eventFromCards(cards) {
Expand All @@ -235,9 +233,9 @@ export function cardsFromEvent(event) {
return cards;
}

function findBoardSize(deck, gameMode, minBoardSize, old) {
function findBoardSize(deck, gameMode, minBoardSize, state) {
let len = Math.min(deck.length, minBoardSize);
while (len < deck.length && !findSet(deck.slice(0, len), gameMode, old))
while (len < deck.length && !findSet(deck.slice(0, len), gameMode, state))
len += 3 - (len % 3);
return len;
}
Expand All @@ -256,7 +254,7 @@ function hasUsedCards(used, cards) {
}

function removeCards(internalGameState, cards) {
const { current, minBoardSize } = internalGameState;
const { current, used, minBoardSize } = internalGameState;
const cutoff = Math.min(current.length - cards.length, minBoardSize);
const cardIndexes = cards
.map((c) => current.indexOf(c))
Expand All @@ -272,41 +270,45 @@ function removeCards(internalGameState, cards) {
break;
}
}
for (const c of cards) {
used[c] = true;
}
}

function processValidEvent(internalGameState, event, cards) {
const { scores, lastEvents, history, used } = internalGameState;
const { scores, lastEvents, history } = internalGameState;
scores[event.user] = (scores[event.user] || 0) + 1;
lastEvents[event.user] = event.time;
history.push(event);
for (const c of cards) {
used[c] = true;
}
}

function updateBoard(internalGameState, cards, old) {
const { current, gameMode, boardSize, minBoardSize } = internalGameState;
function updateBoard(internalGameState, cards) {
const { current, gameMode, boardSize, minBoardSize, findState } =
internalGameState;
// remove cards, preserving positions when possible
removeCards(internalGameState, cards);
// find the new board size
const minSize = Math.max(boardSize - cards.length, minBoardSize);
internalGameState.boardSize = findBoardSize(current, gameMode, minSize, old);
const newSize = findBoardSize(current, gameMode, minSize, findState);
internalGameState.boardSize = newSize;
}

function processEvent(internalGameState, event) {
const { used, chain, history } = internalGameState;
const { used, chain, findState } = internalGameState;
const allCards = cardsFromEvent(event);
let cards;
if (chain && history.length > 0) {
const prev = cardsFromEvent(history[history.length - 1]);
cards = allCards.filter((c) => !prev.includes(c));
if (chain && findState.lastSet.length > 0) {
cards = allCards.filter((c) => !findState.lastSet.includes(c));
if (allCards.length - cards.length !== chain) return;
} else {
cards = allCards;
}
if (hasDuplicates(allCards) || hasUsedCards(used, cards)) return;
if (chain) {
findState.lastSet = allCards;
}
processValidEvent(internalGameState, event, cards);
updateBoard(internalGameState, cards, allCards);
updateBoard(internalGameState, cards);
}

export function computeState(gameData, gameMode) {
Expand All @@ -322,6 +324,8 @@ export function computeState(gameData, gameMode) {
: generateDeck(gameMode, gameData.seed);
const lastEvents = {}; // time of the last event for each user
const minBoardSize = modes[gameMode].minBoardSize;
const chain = modes[gameMode].chain;
const findState = { lastSet: chain ? [] : undefined };
const internalGameState = {
used,
current,
Expand All @@ -330,8 +334,9 @@ export function computeState(gameData, gameMode) {
lastEvents,
gameMode,
minBoardSize,
chain: modes[gameMode].chain,
boardSize: findBoardSize(current, gameMode, minBoardSize, []),
chain,
findState,
boardSize: findBoardSize(current, gameMode, minBoardSize, findState),
};

if (gameData.events) {
Expand All @@ -350,6 +355,7 @@ export function computeState(gameData, gameMode) {
history,
lastEvents,
boardSize: internalGameState.boardSize,
findState,
};
}

Expand Down
22 changes: 11 additions & 11 deletions src/game.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("findSet()", () => {
["1111", "2222", "1010", "2021", "0201", "1021", "1022", "0112"],
]) {
verifySet(findSet(deck, "normal"));
verifySet(findSet(deck, "setchain", []));
verifySet(findSet(deck, "setchain", { lastSet: [] }));
}

for (const deck of [
Expand All @@ -83,14 +83,14 @@ describe("findSet()", () => {
["1121", "2021", "2222", "0112", "1021", "1022", "0201", "1010"],
]) {
expect(findSet(deck, "normal")).toBe(null);
expect(findSet(deck, "setchain", [])).toBe(null);
expect(findSet(deck, "setchain", { lastSet: [] })).toBe(null);
}
});

it("can find set-chains", () => {
const old = ["1200", "0012", "2121"];
expect(findSet(["0112", "0111", "0110"], "setchain", old)).toBe(null);
verifySet(findSet(["0112", "0211", "0110"], "setchain", old));
const state = { lastSet: ["1200", "0012", "2121"] };
expect(findSet(["0112", "0111", "0110"], "setchain", state)).toBe(null);
verifySet(findSet(["0112", "0211", "0110"], "setchain", state));
});

it("can find ultrasets", () => {
Expand All @@ -99,24 +99,24 @@ describe("findSet()", () => {
["1202", "0000", "0001", "0002", "2101"],
]) {
verifyUltra(findSet(deck, "ultraset"));
verifyUltra(findSet(deck, "ultrachain", []));
verifyUltra(findSet(deck, "ultrachain", { lastSet: [] }));
}
for (const deck of [
["1202", "0000", "0001", "0002"],
["1202", "0000", "0001", "0002", "2202"],
]) {
expect(findSet(deck, "ultraset")).toBe(null);
expect(findSet(deck, "ultrachain", [])).toBe(null);
expect(findSet(deck, "ultrachain", { lastSet: [] })).toBe(null);
}
});

it("can find ultraset-chains", () => {
const old = ["1202", "0001", "0002", "2101"];
expect(findSet(["1001", "1221", "1010", "1210"], "ultrachain", old)).toBe(
const state = { lastSet: ["1202", "0001", "0002", "2101"] };
expect(findSet(["1001", "1221", "1010", "1210"], "ultrachain", state)).toBe(
null
);
verifyUltra(findSet(["1001", "1221", "1010", "2112"], "ultrachain", old));
verifyUltra(findSet(["1001", "1221", "1010", "1220"], "ultrachain", old));
verifyUltra(findSet(["1001", "1221", "1010", "2112"], "ultrachain", state));
verifyUltra(findSet(["1001", "1221", "1010", "1220"], "ultrachain", state));
});

it("can find ghostsets", () => {
Expand Down
Loading

0 comments on commit c508e68

Please sign in to comment.