Skip to content

Commit

Permalink
Add ultra9 game mode
Browse files Browse the repository at this point in the history
And some small refactoring.
  • Loading branch information
eltoder committed Dec 12, 2024
1 parent f342a88 commit ed6dce4
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 124 deletions.
4 changes: 2 additions & 2 deletions database.rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"mode": {
".write": "auth != null && auth.uid == data.parent().child('host').val() && newData.exists()",
".validate": "newData.isString() && newData.val().matches(/^normal|ultraset|setchain$/)"
".validate": "newData.isString() && newData.val().matches(/^normal|setchain|ultraset|ultra9$/)"
},
"enableHint": {
".write": "auth != null && auth.uid == data.parent().child('host').val() && newData.exists()",
Expand All @@ -33,7 +33,7 @@
"events": {
"$eventId": {
".write": "!data.exists() && newData.exists() && auth != null && root.hasChild('games/' + $gameId + '/users/' + auth.uid) && root.child('games/' + $gameId + '/status').val() == 'ingame'",
".validate": "newData.hasChildren(['user', 'time', 'c1', 'c2', 'c3']) && (newData.hasChild('c4') == (root.child('games/' + $gameId + '/mode').val() == 'ultraset'))",
".validate": "newData.hasChildren(['user', 'time', 'c1', 'c2', 'c3']) && (newData.hasChild('c4') == (root.child('games/' + $gameId + '/mode').val().matches(/^ultraset|ultra9$/)))",
"user": {
".validate": "newData.isString() && newData.val() == auth.uid"
},
Expand Down
80 changes: 42 additions & 38 deletions functions/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface GameEvent {
c4?: string;
}

export type GameMode = "normal" | "setchain" | "ultraset";
export type GameMode = "normal" | "setchain" | "ultraset" | "ultra9";

/** Generates a random 81-card deck using a Fisher-Yates shuffle. */
export function generateDeck() {
Expand Down Expand Up @@ -80,7 +80,7 @@ export function findSet(deck: string[], gameMode: GameMode, old?: string[]) {
if (old!.includes(c)) {
return [c, deck[i], deck[j]];
}
} else if (gameMode === "ultraset") {
} else if (gameMode === "ultraset" || gameMode === "ultra9") {
if (c in ultraConjugates) {
return [...ultraConjugates[c], deck[i], deck[j]];
}
Expand All @@ -91,58 +91,60 @@ export function findSet(deck: string[], gameMode: GameMode, old?: string[]) {
return null;
}

/** Check if cards are valid (all distinct and exist in deck) */
function isValid(deck: Set<string>, cards: string[]) {
/** Check if all cards are distinct */
function hasDuplicates(cards: string[]) {
for (let i = 0; i < cards.length; i++) {
for (let j = i + 1; j < cards.length; j++) {
if (cards[i] === cards[j]) return false;
if (cards[i] === cards[j]) return true;
}
if (!deck.has(cards[i])) return false;
}
return true;
return false;
}

/** Check if all cards exist in deck */
function validCards(deck: Set<string>, cards: string[]) {
return cards.every((c) => deck.has(c));
}

/** Delete cards from deck */
function deleteCards(deck: Set<string>, cards: string[]) {
for (const c of cards) deck.delete(c);
}

/** Replay game event for normal mode */
function replayEventNormal(deck: Set<string>, event: GameEvent) {
type ReplayFn = (
deck: Set<string>,
event: GameEvent,
history: GameEvent[]
) => boolean;

/** Replay game event for normal and ultra modes */
function replayEventCommon(
deck: Set<string>,
event: GameEvent,
history: GameEvent[]
) {
const cards = [event.c1, event.c2, event.c3];
if (!isValid(deck, cards)) return false;
if (event.c4) cards.push(event.c4);
if (hasDuplicates(cards) || !validCards(deck, cards)) return false;
deleteCards(deck, cards);
return true;
}

/** Replay game event for setchain mode */
function replayEventChain(
history: GameEvent[],
deck: Set<string>,
event: GameEvent
event: GameEvent,
history: GameEvent[]
) {
const { c1, c2, c3 } = event;

// Check validity
let ok = c1 !== c2 && c2 !== c3 && c1 !== c3;
ok &&= deck.has(c2) && deck.has(c3);
const allCards = [c1, c2, c3];
const cards = history.length === 0 ? allCards : allCards.slice(1);
if (hasDuplicates(allCards) || !validCards(deck, cards)) return false;
if (history.length) {
// One card (c1) should be taken from the previous set
const prevEvent = history[history.length - 1];
const prev = [prevEvent.c1, prevEvent.c2, prevEvent.c3];
ok &&= prev.includes(c1);
const prev = history[history.length - 1];
if (![prev.c1, prev.c2, prev.c3].includes(c1)) return false;
}
if (!ok) return;

const cards = history.length === 0 ? [c1, c2, c3] : [c2, c3];
deleteCards(deck, cards);
return true;
}

/** Replay game event for ultraset mode */
function replayEventUltra(deck: Set<string>, event: GameEvent) {
const cards = [event.c1, event.c2, event.c3, event.c4!];
if (!isValid(deck, cards)) return false;
deleteCards(deck, cards);
return true;
}
Expand All @@ -166,15 +168,17 @@ export function replayEvents(
const history: GameEvent[] = [];
const scores: Record<string, number> = {};
let finalTime = 0;
const replayFn: ReplayFn | null =
gameMode === "normal" || gameMode === "ultraset" || gameMode === "ultra9"
? replayEventCommon
: gameMode === "setchain"
? replayEventChain
: null;
if (!replayFn) {
throw new Error(`invalid gameMode ${gameMode}`);
}
for (const event of events) {
let eventValid = false;
if (gameMode === "normal" && replayEventNormal(deck, event))
eventValid = true;
if (gameMode === "setchain" && replayEventChain(history, deck, event))
eventValid = true;
if (gameMode === "ultraset" && replayEventUltra(deck, event))
eventValid = true;
if (eventValid) {
if (replayFn(deck, event, history)) {
history.push(event);
scores[event.user] = (scores[event.user] ?? 0) + 1;
finalTime = event.time;
Expand Down
2 changes: 1 addition & 1 deletion src/components/ChatCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function ChatCards({ item, gameMode, startedAt }) {
<SetCard size="sm" value={item.c3} />
</div>
)}
{gameMode === "ultraset" && (
{(gameMode === "ultraset" || gameMode === "ultra9") && (
<div className={classes.ultraSetCards}>
<div style={{ display: "flex", flexDirection: "column" }}>
<SetCard size="sm" value={item.c1} />
Expand Down
58 changes: 26 additions & 32 deletions src/components/GameSettings.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { makeStyles } from "@material-ui/core/styles";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import Switch from "@material-ui/core/Switch";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";

import firebase from "../firebase";
import { hasHint, modes } from "../util";

const useStyles = makeStyles(() => ({
settings: { display: "flex", flexDirection: "column", alignItems: "center" },
settings: { display: "flex", justifyContent: "space-evenly" },
}));

const hintTip =
Expand All @@ -31,35 +32,28 @@ function GameSettings({ game, gameId, userId }) {

return (
<div className={classes.settings}>
<RadioGroup row value={gameMode} onChange={handleChangeMode}>
{["normal", "setchain", "ultraset"].map((mode) => (
<Tooltip
key={mode}
arrow
placement="left"
title={modes[mode].description}
>
<FormControlLabel
value={mode}
control={<Radio />}
disabled={userId !== game.host}
label={modes[mode].name}
/>
</Tooltip>
))}
</RadioGroup>
{
<Tooltip arrow placement="left" title={hintTip}>
<FormControlLabel
control={<Switch checked={hasHint(game)} onChange={toggleHint} />}
label="Enable Hints"
disabled={
game.access !== "private" ||
Object.keys(game.users || {}).length !== 1
}
/>
</Tooltip>
}
<div style={{ display: "flex", alignItems: "baseline" }}>
<Typography style={{ marginRight: "0.6em" }}>Mode:</Typography>
<Select value={gameMode} onChange={handleChangeMode}>
{Object.entries(modes).map(([key, { name, description }]) => (
<MenuItem key={key} value={key}>
<Tooltip key={key} arrow placement="left" title={description}>
<Typography>{name}</Typography>
</Tooltip>
</MenuItem>
))}
</Select>
</div>
<Tooltip arrow placement="left" title={hintTip}>
<FormControlLabel
control={<Switch checked={hasHint(game)} onChange={toggleHint} />}
label="Enable Hints"
disabled={
game.access !== "private" ||
Object.keys(game.users || {}).length !== 1
}
/>
</Tooltip>
</div>
);
}
Expand Down
11 changes: 5 additions & 6 deletions src/pages/GamePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
findSet,
computeState,
hasHint,
cardsInSet,
} from "../util";
import foundSfx from "../assets/successfulSetSound.mp3";
import failSfx from "../assets/failedSetSound.mp3";
Expand Down Expand Up @@ -141,7 +142,7 @@ function GamePage({ match }) {

const gameMode = game.mode || "normal";
const spectating = !game.users || !(user.id in game.users);
const maxNumHints = gameMode === "ultraset" ? 4 : 3;
const maxNumHints = cardsInSet(gameMode);

const { current, scores, lastEvents, history, boardSize } = computeState(
gameData,
Expand All @@ -155,10 +156,8 @@ function GamePage({ match }) {
);

function handleSet(cards) {
const event =
gameMode === "ultraset"
? { c1: cards[0], c2: cards[1], c3: cards[2], c4: cards[3] }
: { c1: cards[0], c2: cards[1], c3: cards[2] };
const event = { c1: cards[0], c2: cards[1], c3: cards[2] };
if (cards.length === 4) event.c4 = cards[3];
firebase.analytics().logEvent("find_set", event);
firebase
.database()
Expand Down Expand Up @@ -227,7 +226,7 @@ function GamePage({ match }) {
} else {
return vals;
}
} else if (gameMode === "ultraset") {
} else if (gameMode === "ultraset" || gameMode === "ultra9") {
const vals = [...selected, card];
if (vals.length === 4) {
let res = checkSetUltra(...vals);
Expand Down
Loading

0 comments on commit ed6dce4

Please sign in to comment.