Skip to content

Commit e1db71e

Browse files
authored
Add 4Set mode (#139)
1 parent 59a2046 commit e1db71e

10 files changed

+248
-64
lines changed

database.rules.json

+10-10
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
}
1919
},
2020
"mode": {
21-
".write": "auth != null && auth.uid == data.parent().child('host').val() && newData.exists()",
22-
".validate": "newData.isString() && newData.val().matches(/^normal|junior|setchain|ultraset|ultrachain|ultra9|megaset|ghostset|puzzle|shuffle|memory$/)"
21+
".write": "auth != null && auth.uid == data.parent().child('host').val() && data.parent().child('status').val() == 'waiting' && newData.exists()",
22+
".validate": "newData.isString() && newData.val().matches(/^normal|junior|setchain|ultraset|ultrachain|ultra9|megaset|ghostset|4set|puzzle|shuffle|memory$/)"
2323
},
2424
"enableHint": {
25-
".write": "auth != null && auth.uid == data.parent().child('host').val() && newData.exists()",
25+
".write": "auth != null && auth.uid == data.parent().child('host').val() && data.parent().child('status').val() == 'waiting' && newData.exists()",
2626
".validate": "newData.isBoolean() && data.parent().child('access').val() == 'private'"
2727
}
2828
}
@@ -52,30 +52,30 @@
5252
"events": {
5353
"$eventId": {
5454
".write": "!data.exists() && newData.exists() && auth != null && root.hasChild('games/' + $gameId + '/users/' + auth.uid) && root.child('games/' + $gameId + '/status').val() == 'ingame'",
55-
".validate": "newData.hasChildren(['user', 'time', 'c1', 'c2', 'c3']) && (newData.hasChild('c4') == root.child('games/' + $gameId + '/mode').val().matches(/^ultraset|ultrachain|ultra9|ghostset$/)) && (newData.hasChildren(['c5', 'c6']) == (root.child('games/' + $gameId + '/mode').val() == 'ghostset'))",
55+
".validate": "newData.hasChildren(['user', 'time', 'c1', 'c2', 'c3']) && (newData.hasChild('c4') == root.child('games/' + $gameId + '/mode').val().matches(/^ultraset|ultrachain|ultra9|ghostset|4set$/)) && (newData.hasChildren(['c5', 'c6']) == (root.child('games/' + $gameId + '/mode').val() == 'ghostset'))",
5656
"user": {
5757
".validate": "newData.isString() && newData.val() == auth.uid"
5858
},
5959
"time": {
6060
".validate": "newData.isNumber() && newData.val() == now"
6161
},
6262
"c1": {
63-
".validate": "newData.isString() && newData.val().matches(/^[0-2]{3,5}$/)"
63+
".validate": "newData.isString() && newData.val().matches(/^[0-3]{3,5}$/)"
6464
},
6565
"c2": {
66-
".validate": "newData.isString() && newData.val().matches(/^[0-2]{3,5}$/)"
66+
".validate": "newData.isString() && newData.val().matches(/^[0-3]{3,5}$/)"
6767
},
6868
"c3": {
69-
".validate": "newData.isString() && newData.val().matches(/^[0-2]{3,5}$/)"
69+
".validate": "newData.isString() && newData.val().matches(/^[0-3]{3,5}$/)"
7070
},
7171
"c4": {
72-
".validate": "newData.isString() && newData.val().matches(/^[0-2]{3,5}$/)"
72+
".validate": "newData.isString() && newData.val().matches(/^[0-3]{3,5}$/)"
7373
},
7474
"c5": {
75-
".validate": "newData.isString() && newData.val().matches(/^[0-2]{3,5}$/)"
75+
".validate": "newData.isString() && newData.val().matches(/^[0-3]{3,5}$/)"
7676
},
7777
"c6": {
78-
".validate": "newData.isString() && newData.val().matches(/^[0-2]{3,5}$/)"
78+
".validate": "newData.isString() && newData.val().matches(/^[0-3]{3,5}$/)"
7979
},
8080
"$other": {
8181
".validate": false

functions/src/game.ts

+66-15
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ function makeRandom(seed: string) {
6060
let c = parseInt(seed.slice(19, 27), 16) >>> 0;
6161
let d = parseInt(seed.slice(27, 35), 16) >>> 0;
6262
return () => {
63-
let t = b << 9,
64-
r = b * 5;
63+
const t = b << 9;
64+
let r = b * 5;
6565
r = ((r << 7) | (r >>> 25)) * 9;
6666
c ^= a;
6767
d ^= b;
@@ -92,14 +92,15 @@ function shuffleCards(deck: string[], count: number, random: Random) {
9292
}
9393

9494
function generateDeck(gameMode: GameMode, random: Random | null) {
95-
const deck = makeCards(["0", "1", "2"], modes[gameMode].traits);
96-
if (random) {
97-
shuffleCards(deck, deck.length, random);
98-
}
99-
return new Set(deck);
95+
const symbols = Array.from(
96+
Array(setTypes[modes[gameMode].setType].variants),
97+
(_, i) => i + ""
98+
);
99+
const deck = makeCards(symbols, modes[gameMode].traits);
100+
return new Set(random ? shuffleCards(deck, deck.length, random) : deck);
100101
}
101102

102-
/** Returns the unique card c such that {a, b, c} form a set. */
103+
/** Return the unique card c such that {a, b, c} form a set. */
103104
function conjugateCard(a: string, b: string) {
104105
const zeroCode = "0".charCodeAt(0);
105106
let c = "";
@@ -112,25 +113,35 @@ function conjugateCard(a: string, b: string) {
112113
return c;
113114
}
114115

116+
/** Return the unique card d such that {a, b, c, d} form a 4Set. */
117+
function conjugateCard4Set(a: string, b: string, c: string) {
118+
let d = "";
119+
for (let i = 0; i < a.length; i++) {
120+
d += String.fromCharCode(
121+
a.charCodeAt(i) ^ b.charCodeAt(i) ^ c.charCodeAt(i)
122+
);
123+
}
124+
return d;
125+
}
126+
115127
/** Check if three cards form a set. */
116-
export function checkSetNormal(a: string, b: string, c: string) {
128+
function checkSetNormal(a: string, b: string, c: string) {
117129
for (let i = 0; i < a.length; i++) {
118-
if ((a.charCodeAt(i) + b.charCodeAt(i) + c.charCodeAt(i)) % 3 !== 0)
119-
return null;
130+
if ((a.charCodeAt(i) + b.charCodeAt(i) + c.charCodeAt(i)) % 3) return null;
120131
}
121132
return [a, b, c];
122133
}
123134

124135
/** Check if four cards form an ultraset */
125-
export function checkSetUltra(a: string, b: string, c: string, d: string) {
136+
function checkSetUltra(a: string, b: string, c: string, d: string) {
126137
if (conjugateCard(a, b) === conjugateCard(c, d)) return [a, b, c, d];
127138
if (conjugateCard(a, c) === conjugateCard(b, d)) return [a, c, b, d];
128139
if (conjugateCard(a, d) === conjugateCard(b, c)) return [a, d, b, c];
129140
return null;
130141
}
131142

132143
/** Check if six cards form a ghostset */
133-
export function checkSetGhost(
144+
function checkSetGhost(
134145
a: string,
135146
b: string,
136147
c: string,
@@ -146,11 +157,20 @@ export function checkSetGhost(
146157
d.charCodeAt(i) +
147158
e.charCodeAt(i) +
148159
f.charCodeAt(i);
149-
if (sum % 3 !== 0) return null;
160+
if (sum % 3) return null;
150161
}
151162
return [a, b, c, d, e, f];
152163
}
153164

165+
/** Check if four cards form a 4Set */
166+
function checkSet4Set(a: string, b: string, c: string, d: string) {
167+
for (let i = 0; i < a.length; i++) {
168+
if (a.charCodeAt(i) ^ b.charCodeAt(i) ^ c.charCodeAt(i) ^ d.charCodeAt(i))
169+
return null;
170+
}
171+
return [a, b, c, d];
172+
}
173+
154174
function findSetNormal(deck: string[], gameMode: GameMode, state: FindState) {
155175
const deckSet = new Set(deck);
156176
const first =
@@ -221,6 +241,21 @@ function findSetGhost(deck: string[], gameMode: GameMode, state: FindState) {
221241
return null;
222242
}
223243

244+
function findSet4Set(deck: string[], gameMode: GameMode, state: FindState) {
245+
const deckSet = new Set(deck);
246+
for (let i = 0; i < deck.length; i++) {
247+
for (let j = i + 1; j < deck.length; j++) {
248+
for (let k = j + 1; k < deck.length; k++) {
249+
const c = conjugateCard4Set(deck[i], deck[j], deck[k]);
250+
if (deckSet.has(c)) {
251+
return [deck[i], deck[j], deck[k], c];
252+
}
253+
}
254+
}
255+
}
256+
return null;
257+
}
258+
224259
/** Find a set in an unordered collection of cards, if any, depending on mode. */
225260
export function findSet(deck: string[], gameMode: GameMode, state: FindState) {
226261
return setTypes[modes[gameMode].setType].findFn(deck, gameMode, state);
@@ -251,7 +286,7 @@ function findBoard(
251286
state: FindState
252287
) {
253288
const deckIter = deck.values();
254-
let board: string[] = [];
289+
const board: string[] = [];
255290
copyFrom(deckIter, board, minBoardSize);
256291
while (board.length < deck.size && !findSet(board, gameMode, state)) {
257292
copyFrom(deckIter, board, 3 - (board.length % 3));
@@ -316,20 +351,29 @@ function replayEvent(internalGameState: InternalGameState, event: GameEvent) {
316351

317352
const setTypes = {
318353
Set: {
354+
variants: 3,
319355
size: 3,
320356
checkFn: checkSetNormal,
321357
findFn: findSetNormal,
322358
},
323359
UltraSet: {
360+
variants: 3,
324361
size: 4,
325362
checkFn: checkSetUltra,
326363
findFn: findSetUltra,
327364
},
328365
GhostSet: {
366+
variants: 3,
329367
size: 6,
330368
checkFn: checkSetGhost,
331369
findFn: findSetGhost,
332370
},
371+
"4Set": {
372+
variants: 4,
373+
size: 4,
374+
checkFn: checkSet4Set,
375+
findFn: findSet4Set,
376+
},
333377
};
334378

335379
export const modes = {
@@ -389,6 +433,13 @@ export const modes = {
389433
puzzle: false,
390434
minBoardSize: 10,
391435
},
436+
"4set": {
437+
setType: "4Set",
438+
traits: 4,
439+
chain: 0,
440+
puzzle: false,
441+
minBoardSize: 15,
442+
},
392443
puzzle: {
393444
setType: "Set",
394445
traits: 4,

public/index.html

+27-6
Original file line numberDiff line numberDiff line change
@@ -44,31 +44,52 @@
4444
<defs>
4545
<path
4646
id="squiggle"
47-
d="M67.8929 12.7468C24.6616 19.464-8.7567 61.8274 18.9828 71.747c27.7395 9.9196 46.4867 12.1945 49.6132 80.1771 3.1264 67.9826-63.7587 148.0132-46.1482 188.092 17.6104 40.0788 82.2574 59.8106 143.7329 41.9338 61.4755-17.8768-17.7637-48.8463-22.3299-105.806.2225-43.1835 58.7474-152.5141 45.0712-204.6507-13.6762-52.1366-77.7977-65.4636-121.029-58.7464Z"
47+
d="m67.892902,12.746785c43.231313,-6.717223 107.352741,6.609823 121.028973,58.746408c13.676233,52.136585 -44.848649,161.467192 -45.07116,204.650732c4.566246,56.959708 83.805481,87.929227 22.329944,105.806022c-61.475536,17.876795 -126.122496,-1.855045 -143.73294,-41.933823c-17.610444,-40.07878 49.274638,-120.109409 46.14822,-188.091997c-3.126418,-67.982588 -21.873669,-70.257464 -49.613153,-80.177084c-27.739485,-9.919618 5.678801,-52.283035 48.910115,-59.000258z"
4848
/>
4949
<path
5050
id="oval"
51-
d="m11.5 95.8666c0-44.5571 37.4429-82 82-82h12c44.5571 0 82 37.4429 82 82V302c0 44.5571-37.4429 82-82 82h-12c-44.5571 0-82-37.4429-82-82V95.8666z"
51+
d="m11.49999,95.866646c0,-44.557076 37.442923,-81.999998 82.000002,-81.999998l12.000015,0c44.557076,0 81.999992,37.442923 81.999992,81.999998l0,206.133354c0,44.557098 -37.442917,82 -81.999992,82l-12.000015,0c-44.557079,0 -82.000002,-37.442902 -82.000002,-82l0,-206.133354z"
5252
/>
5353
<path
5454
id="diamond"
55-
d="m98.5445 10.3119-87.8302 189.3308 88.2011 189.6444 88.9423-190.3627L98.5445 10.3119z"
55+
d="m98.544521,10.311863l-87.830189,189.330815l88.201143,189.644391l88.942329,-190.362741l-89.313283,-188.612465z"
5656
/>
57+
<path id="triangle" d="m40 10.3119 0 190 0 190 130-190-130-190z" />
5758
<pattern
58-
id="pattern-stripe"
59+
id="pattern-striped"
5960
width="2"
6061
height="20"
6162
patternUnits="userSpaceOnUse"
6263
>
6364
<rect width="2" height="8" fill="#fff" />
6465
</pattern>
65-
<mask id="mask-stripe">
66+
<mask id="mask-striped">
6667
<rect
6768
x="0"
6869
y="0"
6970
width="200"
7071
height="400"
71-
fill="url(#pattern-stripe)"
72+
fill="url(#pattern-striped)"
73+
/>
74+
</mask>
75+
<pattern
76+
id="pattern-dotted"
77+
width="50"
78+
height="50"
79+
patternUnits="userSpaceOnUse"
80+
>
81+
<circle cx="0" cy="25" r="12" fill="#fff" />
82+
<circle cx="25" cy="0" r="12" fill="#fff" />
83+
<circle cx="50" cy="25" r="12" fill="#fff" />
84+
<circle cx="25" cy="50" r="12" fill="#fff" />
85+
</pattern>
86+
<mask id="mask-dotted">
87+
<rect
88+
x="0"
89+
y="0"
90+
width="200"
91+
height="400"
92+
fill="url(#pattern-dotted)"
7293
/>
7394
</mask>
7495
</defs>

src/components/ChatCards.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function ChatCards({ item, gameMode, startedAt }) {
6666
</div>
6767
{cards && (
6868
<div className={classes.setCards}>
69-
{setType === "Set" &&
69+
{(setType === "Set" || setType === "4Set") &&
7070
cards.map((card) => (
7171
<SetCard key={card} size="sm" value={card} />
7272
))}

src/components/ColorChoiceDialog.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -27,49 +27,58 @@ function ColorChoiceDialog(props) {
2727
const [red, setRed] = useState(theme.setCard.red);
2828
const [green, setGreen] = useState(theme.setCard.green);
2929
const [purple, setPurple] = useState(theme.setCard.purple);
30+
const [orange, setOrange] = useState(theme.setCard.orange);
3031

3132
function handleClose() {
3233
onClose(null);
3334
}
3435

3536
function handleSubmit() {
36-
onClose({ red, green, purple });
37+
onClose({ red, green, purple, orange });
3738
}
3839

3940
function handleReset() {
4041
const resetTo = theme.palette.type === "light" ? lightTheme : darkTheme;
4142
setRed(resetTo.setCard.red);
4243
setGreen(resetTo.setCard.green);
4344
setPurple(resetTo.setCard.purple);
45+
setOrange(resetTo.setCard.orange);
4446
}
4547

4648
return (
4749
<Dialog open={open} onClose={handleClose} maxWidth="xl">
4850
<DialogTitle>{title}</DialogTitle>
4951
<DialogContent>
5052
<ThemeProvider theme={withCardColors(theme, { red, green, purple })}>
51-
<Grid container spacing={3}>
52-
<Grid item xs={12} md={4} className={classes.colorPickerColumn}>
53+
<Grid container spacing={2}>
54+
<Grid item xs={12} md={3} className={classes.colorPickerColumn}>
5355
<ResponsiveSetCard width={225} value="0000" />
5456
<ChromePicker
5557
color={purple}
5658
onChangeComplete={(result) => setPurple(result.hex)}
5759
/>
5860
</Grid>
59-
<Grid item xs={12} md={4} className={classes.colorPickerColumn}>
61+
<Grid item xs={12} md={3} className={classes.colorPickerColumn}>
6062
<ResponsiveSetCard width={225} value="1000" />
6163
<ChromePicker
6264
color={green}
6365
onChangeComplete={(result) => setGreen(result.hex)}
6466
/>
6567
</Grid>
66-
<Grid item xs={12} md={4} className={classes.colorPickerColumn}>
68+
<Grid item xs={12} md={3} className={classes.colorPickerColumn}>
6769
<ResponsiveSetCard width={225} value="2000" />
6870
<ChromePicker
6971
color={red}
7072
onChangeComplete={(result) => setRed(result.hex)}
7173
/>
7274
</Grid>
75+
<Grid item xs={12} md={3} className={classes.colorPickerColumn}>
76+
<ResponsiveSetCard width={225} value="3000" />
77+
<ChromePicker
78+
color={orange}
79+
onChangeComplete={(result) => setOrange(result.hex)}
80+
/>
81+
</Grid>
7382
</Grid>
7483
<Grid container direction="row" justifyContent="center">
7584
<Button

0 commit comments

Comments
 (0)