Skip to content

Commit a567dee

Browse files
author
acornie
committed
feat: support game wins (#16)
* feat: support game wins * chore: clean up
1 parent c2079b6 commit a567dee

7 files changed

Lines changed: 98 additions & 3 deletions

File tree

components/board.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import styles from "@/styles/board.module.css";
44
import Tile from "./tile";
55
import { GameContext } from "@/context/game-context";
66
import MobileSwiper, { SwipeInput } from "./mobile-swiper";
7+
import Splash from "./splash";
78

89
export default function Board() {
9-
const { getTiles, moveTiles, startGame } = useContext(GameContext);
10+
const { getTiles, moveTiles, startGame, status } = useContext(GameContext);
1011
const initialized = useRef(false);
1112

1213
const handleKeyDown = useCallback(
@@ -86,6 +87,7 @@ export default function Board() {
8687
return (
8788
<MobileSwiper onSwipe={handleSwipe}>
8889
<div className={styles.board}>
90+
{status === "won" && <Splash heading="You won!" />}
8991
<div className={styles.tiles}>{renderTiles()}</div>
9092
<div className={styles.grid}>{renderGrid()}</div>
9193
</div>

components/splash.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { GameContext } from "@/context/game-context";
2+
import styles from "@/styles/splash.module.css";
3+
import { useContext } from "react";
4+
5+
export default function Splash({ heading = "You won!" }) {
6+
const { startGame } = useContext(GameContext);
7+
8+
return (
9+
<div className={`${styles.splash} ${styles.win}`}>
10+
<div>
11+
<h1>{heading}</h1>
12+
<button className={styles.button} onClick={startGame}>
13+
Play again
14+
</button>
15+
</div>
16+
</div>
17+
);
18+
}

constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ export const tileCountPerDimension = 4;
1313
export const mergeAnimationDuration = 100; // ms
1414

1515
export const moveAnimationDuration = 200; // ms
16+
17+
/**
18+
* Game setup
19+
*/
20+
export const gameWinTileValue = 2048;

context/game-context.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import {
66
useReducer,
77
} from "react";
88
import { isNil, throttle } from "lodash";
9-
import { mergeAnimationDuration, tileCountPerDimension } from "@/constants";
9+
import {
10+
gameWinTileValue,
11+
mergeAnimationDuration,
12+
tileCountPerDimension,
13+
} from "@/constants";
1014
import { Tile } from "@/models/tile";
1115
import gameReducer, { initialState } from "@/reducers/game-reducer";
1216

1317
type MoveDirection = "move_up" | "move_down" | "move_left" | "move_right";
1418

1519
export const GameContext = createContext({
1620
score: 0,
21+
status: "ongoing",
1722
moveTiles: (_: MoveDirection) => {},
1823
getTiles: () => [] as Tile[],
1924
startGame: () => {},
@@ -61,15 +66,27 @@ export default function GameProvider({ children }: PropsWithChildren) {
6166
);
6267

6368
const startGame = () => {
69+
dispatch({ type: "reset_game" });
6470
dispatch({ type: "create_tile", tile: { position: [0, 1], value: 2 } });
6571
dispatch({ type: "create_tile", tile: { position: [0, 2], value: 2 } });
6672
};
6773

74+
const checkGameState = () => {
75+
const isWon =
76+
Object.values(gameState.tiles).filter((t) => t.value === gameWinTileValue)
77+
.length > 0;
78+
79+
if (isWon) {
80+
dispatch({ type: "update_status", status: "won" });
81+
}
82+
};
83+
6884
useEffect(() => {
6985
if (gameState.hasChanged) {
7086
setTimeout(() => {
7187
dispatch({ type: "clean_up" });
7288
appendRandomTile();
89+
checkGameState();
7390
}, mergeAnimationDuration);
7491
}
7592
}, [gameState.hasChanged]);
@@ -78,6 +95,7 @@ export default function GameProvider({ children }: PropsWithChildren) {
7895
<GameContext.Provider
7996
value={{
8097
score: gameState.score,
98+
status: gameState.status,
8199
getTiles,
82100
moveTiles,
83101
startGame,

reducers/game-reducer.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@ import { uid } from "uid";
33
import { tileCountPerDimension } from "@/constants";
44
import { Tile, TileMap } from "@/models/tile";
55

6+
type GameStatus = "ongoing" | "won";
7+
68
type State = {
79
board: string[][];
810
tiles: TileMap;
911
tilesByIds: string[];
1012
hasChanged: boolean;
1113
score: number;
14+
status: GameStatus;
1215
};
1316
type Action =
1417
| { type: "create_tile"; tile: Tile }
1518
| { type: "clean_up" }
1619
| { type: "move_up" }
1720
| { type: "move_down" }
1821
| { type: "move_left" }
19-
| { type: "move_right" };
22+
| { type: "move_right" }
23+
| { type: "reset_game" }
24+
| { type: "update_status"; status: GameStatus };
2025

2126
function createBoard() {
2227
const board: string[][] = [];
@@ -34,6 +39,7 @@ export const initialState: State = {
3439
tilesByIds: [],
3540
hasChanged: false,
3641
score: 0,
42+
status: "ongoing",
3743
};
3844

3945
export default function gameReducer(
@@ -287,6 +293,13 @@ export default function gameReducer(
287293
score,
288294
};
289295
}
296+
case "reset_game":
297+
return initialState;
298+
case "update_status":
299+
return {
300+
...state,
301+
status: action.status,
302+
};
290303
default:
291304
return state;
292305
}

styles/globals.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
--secondary-background: #bbada0;
77
--cell-background: #cac1b5;
88
--tile-background: #eee4da;
9+
--button-background: #8f7a66;
910

1011
/* Colors */
1112
--primary-text-color: #776e65;

styles/splash.module.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.splash {
2+
display: flex;
3+
justify-content: center;
4+
align-items: center;
5+
position: absolute;
6+
text-align: center;
7+
z-index: 3;
8+
top: 0;
9+
left: 0;
10+
right: 0;
11+
bottom: 0;
12+
border-radius: calc(var(--pixel-size) * 0.75);
13+
background: rgba(238, 228, 218, 0.6);
14+
}
15+
16+
.win {
17+
background: rgba(237, 194, 46, 0.6);
18+
color: white;
19+
}
20+
21+
.splash > div {
22+
margin: 0 auto;
23+
}
24+
25+
.splash > div > button {
26+
margin: calc(var(--pixel-size) * 3) auto;
27+
}
28+
29+
.button {
30+
background: var(--button-background);
31+
border: calc(var(--pixel-size) * 0.5) solid var(--button-background);
32+
border-radius: calc(var(--pixel-size) * 0.5);
33+
font-size: calc(var(--pixel-size) * 2);
34+
line-height: calc(var(--pixel-size) * 4);
35+
font-weight: bold;
36+
color: var(--tile-text-color);
37+
cursor: pointer;
38+
}

0 commit comments

Comments
 (0)