Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add animation and chat messages for puzzle mode #130

Merged
merged 1 commit into from
Feb 19, 2025
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
66 changes: 39 additions & 27 deletions src/components/ChatCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";

import { cardsFromEvent, checkSetUltra, conjugateCard, modes } from "../game";
import { formatTime } from "../util";
import { formatCount, formatTime } from "../util";
import SetCard from "./SetCard";
import User from "./User";

Expand Down Expand Up @@ -35,40 +35,52 @@ const useStyles = makeStyles((theme) => ({
function ChatCards({ item, gameMode, startedAt }) {
const classes = useStyles();
const setType = modes[gameMode].setType;
let cards = cardsFromEvent(item);
if (setType === "UltraSet") {
// Arrange cards in pairs and add the 5th card
cards = checkSetUltra(...cards) || cards.slice();
cards.splice(2, 0, conjugateCard(cards[0], cards[1]), null);
let cards;
if (!item.kind) {
cards = cardsFromEvent(item);
if (setType === "UltraSet") {
// Arrange cards in pairs and add the 5th card
cards = checkSetUltra(...cards) || cards.slice();
cards.splice(2, 0, conjugateCard(cards[0], cards[1]), null);
}
}

return (
<Tooltip arrow placement="left" title={formatTime(item.time - startedAt)}>
<div className={classes.logEntry}>
<div className={classes.logEntryText}>
<Typography variant="subtitle2" style={{ marginRight: "0.2em" }}>
{setType} found by
<Typography variant="subtitle2">
{item.kind === "board_done"
? `Board clear: ${formatCount(item.count, setType)}`
: `${setType} found by`}
</Typography>
<User
component={Typography}
noWrap={true}
variant="subtitle2"
id={item.user}
/>
</div>
<div className={classes.setCards}>
{setType === "Set" &&
cards.map((card) => <SetCard key={card} size="sm" value={card} />)}
{(setType === "UltraSet" || setType === "GhostSet") &&
Array.from(Array(3), (_, i) => (
<div key={i} className={classes.cardsColumn}>
<SetCard size="sm" value={cards[i * 2]} />
{cards[i * 2 + 1] && (
<SetCard size="sm" value={cards[i * 2 + 1]} />
)}
</div>
))}
{item.user && (
<User
component={Typography}
noWrap={true}
variant="subtitle2"
id={item.user}
style={{ marginLeft: "0.2em" }}
/>
)}
</div>
{cards && (
<div className={classes.setCards}>
{setType === "Set" &&
cards.map((card) => (
<SetCard key={card} size="sm" value={card} />
))}
{(setType === "UltraSet" || setType === "GhostSet") &&
Array.from(Array(3), (_, i) => (
<div key={i} className={classes.cardsColumn}>
<SetCard size="sm" value={cards[i * 2]} />
{cards[i * 2 + 1] && (
<SetCard size="sm" value={cards[i * 2 + 1]} />
)}
</div>
))}
</div>
)}
</div>
</Tooltip>
);
Expand Down
14 changes: 13 additions & 1 deletion src/components/Game.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useMemo } from "react";
import { useContext, useEffect, useMemo, useState } from "react";

import Divider from "@material-ui/core/Divider";
import Paper from "@material-ui/core/Paper";
Expand Down Expand Up @@ -31,6 +31,7 @@ function Game({
selected,
answer,
lastSet,
lastKeptSet,
faceDown,
showShortcuts,
remaining = -1,
Expand All @@ -40,6 +41,16 @@ function Game({
const isHorizontal = cardOrientation === "horizontal";
const isLandscape = layoutOrientation === "landscape";
const [gameDimensions, gameEl] = useDimensions();
const [highlightCards, setHighlightCards] = useState(null);

const lastKeptCards = lastKeptSet?.join("|");
useEffect(() => {
setHighlightCards(lastKeptCards?.split("|"));
if (lastKeptCards) {
const timer = setTimeout(() => setHighlightCards(null), 500);
return () => clearTimeout(timer);
}
}, [lastKeptCards]);

[board, lastSet] = useMemo(
() => addLastSet(board, lastSet),
Expand Down Expand Up @@ -249,6 +260,7 @@ function Game({
width={cardWidth}
hinted={answer?.includes(card)}
active={selected?.includes(card)}
highlight={highlightCards?.includes(card)}
faceDown={
faceDown === "all" ||
(faceDown &&
Expand Down
10 changes: 7 additions & 3 deletions src/components/ResponsiveSetCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const useStyles = makeStyles((theme) => ({
justifyContent: "center",
flexShrink: 0,
backgroundColor: theme.setCard.background,
transition: "box-shadow 0.15s",
transition:
"box-shadow 0.15s, width 0.5s, height 0.5s, background-color 0.3s",
},
clickable: {
cursor: "pointer",
Expand All @@ -31,6 +32,9 @@ const useStyles = makeStyles((theme) => ({
active: {
boxShadow: "0px 0px 5px 3px #4b9e9e !important",
},
highlight: {
backgroundColor: theme.setCard.highlight,
},
hintedOverlay: {
position: "absolute",
inset: 0,
Expand Down Expand Up @@ -71,7 +75,7 @@ function ResponsiveSetCard(props) {
const theme = useTheme();

// Black magic below to scale cards given any width
const { width, value, onClick, hinted, active, faceDown } = props;
const { width, value, onClick, hinted, active, highlight, faceDown } = props;
const height = Math.round(width / 1.6);
const margin = Math.round(width * 0.035);
const contentWidth = width - 2 * margin;
Expand Down Expand Up @@ -109,6 +113,7 @@ function ResponsiveSetCard(props) {
className={clsx(classes.card, {
[classes.clickable]: onClick,
[classes.active]: active,
[classes.highlight]: highlight,
})}
style={{
...extraStyle,
Expand All @@ -118,7 +123,6 @@ function ResponsiveSetCard(props) {
margin: margin,
borderRadius: margin,
border: faceDown ? undefined : BORDERS[border],
transition: "width 0.5s, height 0.5s",
}}
onClick={onClick}
>
Expand Down
20 changes: 16 additions & 4 deletions src/game.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { formatCount } from "./util";

export const BASE_RATING = 1200;
export const SCALING_FACTOR = 800;

Expand Down Expand Up @@ -108,11 +110,10 @@ export function addCard(deck, card, gameMode, findState) {
if (doChain) {
const fromLast = cards.reduce((s, c) => s + lastSet.includes(c), 0);
if (fromLast !== chain) {
const noun = chain > 1 ? "cards" : "card";
return {
kind: "error",
cards,
error: `${chain} ${noun} must be from the previous ${setType}`,
error: `${formatCount(chain, "card")} must be from the previous ${setType}`,
};
}
}
Expand Down Expand Up @@ -295,7 +296,7 @@ function processValidEvent(internalGameState, event, cards) {
history.push(event);
}

function updateBoard(internalGameState, cards) {
function updateBoard(internalGameState, event, cards) {
const { current, gameMode, puzzle, boardSize, minBoardSize, findState } =
internalGameState;
// in puzzle modes only advance after all sets were found
Expand All @@ -304,6 +305,17 @@ function updateBoard(internalGameState, cards) {
if (findSet(board, gameMode, findState)) return;
findState.foundSets.clear();
cards = board;
// add a synthetic event to show in chat that the board changed
const { history } = internalGameState;
let i;
for (i = history.length - 2; i >= 0; i--) {
if (history[i].kind === "board_done") break;
}
history.push({
time: event.time,
kind: "board_done",
count: history.length - 1 - i,
});
}
// remove cards, preserving positions when possible
removeCards(internalGameState, cards);
Expand Down Expand Up @@ -333,7 +345,7 @@ function processEvent(internalGameState, event) {
findState.lastSet = allCards;
}
processValidEvent(internalGameState, event, cards);
updateBoard(internalGameState, cards);
updateBoard(internalGameState, event, cards);
}

export function computeState(gameData, gameMode) {
Expand Down
35 changes: 26 additions & 9 deletions src/pages/GamePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import { SettingsContext, UserContext } from "../context";
import firebase, { createGame, finishGame } from "../firebase";
import {
addCard,
cardsFromEvent,
computeState,
eventFromCards,
findSet,
generateDeck,
modes,
removeCard,
} from "../game";
import useFirebaseRef from "../hooks/useFirebaseRef";
Expand Down Expand Up @@ -142,15 +144,29 @@ function GamePage({ match }) {
);
}, [gameMode, gameData?.deck, gameData?.seed]);

const { current, scores, lastEvents, history, board, answer, findState } =
useMemo(() => {
if (!gameData) return {};
const state = computeState({ ...gameData, deck }, gameMode);
const { current, boardSize, findState } = state;
const board = current.slice(0, boardSize);
const answer = findSet(board, gameMode, findState);
return { ...state, board, answer };
}, [gameMode, gameData, deck]);
const {
current,
scores,
lastEvents,
history,
board,
answer,
findState,
lastKeptSet,
} = useMemo(() => {
if (!gameData) return {};
const state = computeState({ ...gameData, deck }, gameMode);
const { current, boardSize, findState, history } = state;
const board = current.slice(0, boardSize);
const answer = findSet(board, gameMode, findState);
const lastKeptSet =
modes[gameMode].puzzle &&
history.length &&
!history[history.length - 1].kind
? cardsFromEvent(history[history.length - 1])
: null;
return { ...state, board, answer, lastKeptSet };
}, [gameMode, gameData, deck]);

if (redirect) return <Redirect push to={redirect} />;

Expand Down Expand Up @@ -414,6 +430,7 @@ function GamePage({ match }) {
onClick={handleClick}
onClear={handleClear}
lastSet={findState.lastSet}
lastKeptSet={lastKeptSet}
answer={hint}
remaining={current.length - board.length}
faceDown={paused ? "all" : gameMode === "memory" ? "deal" : ""}
Expand Down
6 changes: 3 additions & 3 deletions src/themes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { grey } from "@material-ui/core/colors";
import { indigo } from "@material-ui/core/colors";
import { red } from "@material-ui/core/colors";
import { grey, indigo, red } from "@material-ui/core/colors";
import { createTheme } from "@material-ui/core/styles";

export const darkTheme = createTheme({
Expand Down Expand Up @@ -48,6 +46,7 @@ export const darkTheme = createTheme({
red: "#ffb047",
background: "#262626",
hinted: "rgba(41, 182, 246, 0.25)",
highlight: "rgba(75, 158, 158, 0.3)",
backColors: [
grey[900],
grey[800],
Expand Down Expand Up @@ -96,6 +95,7 @@ export const lightTheme = createTheme({
red: "#ff0101",
background: "#fff",
hinted: "rgba(3, 169, 244, 0.2)",
highlight: "rgba(255, 234, 0, 0.3)",
backColors: [
indigo[600],
indigo[300],
Expand Down
5 changes: 5 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export function formatTime(t, hideSubsecond) {
return (hours ? `${hours}:` : "") + moment.utc(rest).format(format);
}

export function formatCount(count, singular, plural = null) {
const noun = count === 1 ? singular : (plural ?? singular + "s");
return `${count} ${noun}`;
}

export function censorText(text) {
return censor.applyTo(text, badWords.getAllMatches(text));
}
Expand Down