Skip to content
Open
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: 4 additions & 0 deletions functions/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ function deleteCards(deck: Set<string>, cards: string[]) {
function replayEventNormal(deck: Set<string>, event: GameEvent) {
const cards = [event.c1, event.c2, event.c3];
if (!isValid(deck, cards)) return false;
// Cards have to actually form a set, not just be three distinct cards on the board
if (!checkSet(event.c1, event.c2, event.c3)) return false;
deleteCards(deck, cards);
return true;
}
Expand All @@ -148,6 +150,7 @@ function replayEventChain(
const prev = [prevEvent.c1, prevEvent.c2, prevEvent.c3];
ok &&= prev.includes(c1);
}
ok &&= checkSet(c1, c2, c3);
if (!ok) return;

const cards = history.length === 0 ? [c1, c2, c3] : [c2, c3];
Expand All @@ -159,6 +162,7 @@ function replayEventChain(
function replayEventUltra(deck: Set<string>, event: GameEvent) {
const cards = [event.c1, event.c2, event.c3, event.c4!];
if (!isValid(deck, cards)) return false;
if (!checkSetUltra(event.c1, event.c2, event.c3, event.c4!)) return false;
deleteCards(deck, cards);
return true;
}
Expand Down
4 changes: 4 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ function processEventNormal(internalGameState, event) {
const { current, used } = internalGameState;
const cards = [event.c1, event.c2, event.c3];
if (hasDuplicates(used, cards)) return;
// Cards have to actually form a set, not just be three distinct cards on the board
if (!checkSet(event.c1, event.c2, event.c3)) return;
processValidEvent(internalGameState, event, cards);

const minSize = Math.max(internalGameState.boardSize - 3, 12);
Expand All @@ -276,6 +278,7 @@ function processEventChain(internalGameState, event) {
} else {
ok &&= !used[c1];
}
ok &&= checkSet(c1, c2, c3);
if (!ok) return;

const cards = history.length === 0 ? [c1, c2, c3] : [c2, c3];
Expand All @@ -291,6 +294,7 @@ function processEventUltra(internalGameState, event) {
const { used, current } = internalGameState;
const cards = [event.c1, event.c2, event.c3, event.c4];
if (hasDuplicates(used, cards)) return;
if (!checkSetUltra(event.c1, event.c2, event.c3, event.c4)) return;
processValidEvent(internalGameState, event, cards);

const minSize = Math.max(internalGameState.boardSize - 4, 12);
Expand Down
75 changes: 75 additions & 0 deletions src/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
checkSet,
checkSetUltra,
computeState,
conjugateCard,
filter,
findSet,
Expand Down Expand Up @@ -87,6 +88,80 @@ it("initializes junior deck", () => {
expect(initializeDeck(generateCards(), "ultraset")).has.length(81);
});

describe("computeState() rejects non-sets", () => {
function gameData(deck, events) {
return {
deck,
events: Object.fromEntries(
events.map((e, i) => [String(i), { ...e, time: i + 1 }]),
),
};
}

it("normal mode ignores three cards that aren't a set", () => {
const state = computeState(
gameData(generateCards(), [
{ c1: "0000", c2: "0001", c3: "0010", user: "u" },
]),
"normal",
);
expect(state.history).toHaveLength(0);
expect(state.scores).toEqual({});
});

it("normal mode still accepts a real set", () => {
const state = computeState(
gameData(generateCards(), [
{ c1: "0001", c2: "0002", c3: "0000", user: "u" },
]),
"normal",
);
expect(state.scores["u"]).toBe(1);
});

it("setchain mode ignores a non-set opening", () => {
const state = computeState(
gameData(generateCards(), [
{ c1: "0000", c2: "0001", c3: "0010", user: "u" },
]),
"setchain",
);
expect(state.history).toHaveLength(0);
});

it("setchain mode ignores a non-set continuation", () => {
const state = computeState(
gameData(generateCards(), [
{ c1: "0001", c2: "0002", c3: "0000", user: "u1" },
{ c1: "0000", c2: "0011", c3: "0021", user: "u2" },
]),
"setchain",
);
expect(state.history).toHaveLength(1);
expect(state.scores["u2"]).toBeUndefined();
});

it("ultraset mode ignores four cards that aren't an ultraset", () => {
const state = computeState(
gameData(generateCards(), [
{ c1: "0000", c2: "1111", c3: "2222", c4: "1212", user: "u" },
]),
"ultraset",
);
expect(state.history).toHaveLength(0);
});

it("ultraset mode still accepts a real ultraset", () => {
const state = computeState(
gameData(generateCards(), [
{ c1: "0001", c2: "0002", c3: "1202", c4: "2101", user: "u" },
]),
"ultraset",
);
expect(state.scores["u"]).toBe(1);
});
});

describe("bad-words filter", () => {
it("does not trigger on 'wang'", () => {
expect(filter.isProfane("Rona Wang")).toBe(false);
Expand Down