comparison react_games/src/CardMatchiing/main.tsx @ 37:fb9bcd3145cb

[ReactGames] Few games I made using react just to practice few things.
author MrJuneJune <me@mrjunejune.com>
date Mon, 01 Dec 2025 20:22:47 -0800
parents
children
comparison
equal deleted inserted replaced
36:84672efec192 37:fb9bcd3145cb
1 /**
2 *
3 * You’re tasked with building a simple memory card-matching game in React.
4
5 Each card has a hidden value, and the player flips two cards at a time to try to find a match.
6
7 If the cards match, they stay face-up. If not, they flip back after a short delay.
8
9 The game ends when all pairs are matched.
10
11 // Basic components
12 // Board
13 // Cards (values 1 to 10)
14 // There will be 20 cards, 4 x 5.
15 //
16 // GameState: playable or not
17 // logic check if the value sames
18 //
19 // listOfCurrentlyTurnOpend: len(2) number[]
20 */
21 import React, {
22 createContext,
23 useContext,
24 useEffect,
25 useReducer,
26 } from "react";
27
28 /* ──────────────────── Types ──────────────────── */
29 interface CardValue {
30 value: number;
31 isFacing: boolean;
32 isSolved: boolean;
33 }
34
35 interface CardProp {
36 row: number;
37 col: number;
38 card: CardValue;
39 }
40
41 interface RowProp {
42 rowPos: number;
43 row: CardValue[];
44 }
45
46 type Action =
47 | { type: "move"; row: number; col: number }
48 | { type: "flipBack" }
49 | { type: "reset" };
50
51 interface Position {
52 row: number;
53 col: number;
54 }
55
56 interface GameState {
57 board: CardValue[][];
58 open: Position[]; // cards currently face-up but not yet decided (max 2)
59 busy: boolean; // UI locked while we wait to flip cards back
60 }
61
62 /* ──────────────────── Helpers ──────────────────── */
63 const shuffle = <T,>(arr: T[]): T[] =>
64 [...arr].sort(() => Math.random() - 0.5);
65
66 const makeBoard = (): CardValue[][] => {
67 // two of each from 1-10, then shuffle and slice into 4×5
68 const values = shuffle(
69 Array.from({ length: 10 }, (_, i) => i + 1).flatMap((v) => [v, v])
70 );
71
72 return Array.from({ length: 4 }, (_, r) =>
73 Array.from({ length: 5 }, (_, c) => ({
74 value: values[r * 5 + c],
75 isFacing: false,
76 isSolved: false,
77 }))
78 );
79 };
80
81 /* ──────────────────── Context ──────────────────── */
82 const BoardContext = createContext<React.Dispatch<Action> | null>(null);
83 const useBoardDispatch = () => {
84 const ctx = useContext(BoardContext);
85 if (!ctx) throw new Error("BoardContext missing");
86 return ctx;
87 };
88
89 /* ──────────────────── Reducer ──────────────────── */
90 const initial = (): GameState => ({ board: makeBoard(), open: [], busy: false });
91
92 function reducer(state: GameState, action: Action): GameState {
93 switch (action.type) {
94 case "reset":
95 return initial();
96
97 case "flipBack": {
98 // hide the two open cards
99 const [a, b] = state.open;
100 const nextBoard = state.board.map((row, r) =>
101 row.map((card, c) =>
102 (r === a.row && c === a.col) || (r === b.row && c === b.col)
103 ? { ...card, isFacing: false }
104 : card
105 )
106 );
107 return { board: nextBoard, open: [], busy: false };
108 }
109
110 case "move": {
111 if (state.busy) return state; // ignore clicks while waiting
112
113 const { row, col } = action;
114 const target = state.board[row][col];
115 if (target.isFacing || target.isSolved) return state;
116
117 // flip this one up
118 const nextBoard = state.board.map((r, ri) =>
119 r.map((c, ci) =>
120 ri === row && ci === col ? { ...c, isFacing: true } : c
121 )
122 );
123 const open = [...state.open, { row, col }];
124
125 if (open.length < 2) return { ...state, board: nextBoard, open };
126
127 // now we have two cards – decide match / mismatch
128 const [a, b] = open;
129 const first = nextBoard[a.row][a.col];
130 const second = nextBoard[b.row][b.col];
131
132 if (first.value === second.value) {
133 // match → mark solved, leave face-up
134 const solvedBoard = nextBoard.map((r, ri) =>
135 r.map((c, ci) =>
136 (ri === a.row && ci === a.col) || (ri === b.row && ci === b.col)
137 ? { ...c, isSolved: true }
138 : c
139 )
140 );
141 return { board: solvedBoard, open: [], busy: false };
142 }
143
144 // mismatch → leave them up temporarily, then flip back via effect
145 return { board: nextBoard, open, busy: true };
146 }
147
148 default:
149 return state;
150 }
151 }
152
153 /* ──────────────────── UI Components ──────────────────── */
154 const Card = ({ row, col, card }: CardProp) => {
155 const dispatch = useBoardDispatch();
156 const style: React.CSSProperties = {
157 width: 60,
158 height: 80,
159 margin: 4,
160 fontSize: 24,
161 display: "flex",
162 alignItems: "center",
163 justifyContent: "center",
164 background: card.isSolved
165 ? "#8bc34a"
166 : card.isFacing
167 ? "#fff"
168 : "#90caf9",
169 cursor: card.isSolved ? "default" : "pointer",
170 borderRadius: 6,
171 userSelect: "none",
172 };
173 return (
174 <div style={style} onClick={() => dispatch({ type: "move", row, col })}>
175 {card.isFacing || card.isSolved ? card.value : "🂠"}
176 </div>
177 );
178 };
179
180 const Row = ({ row, rowPos }: RowProp) => (
181 <div style={{ display: "flex" }}>
182 {row.map((card, idx) => (
183 <Card key={idx} row={rowPos} col={idx} card={card} />
184 ))}
185 </div>
186 );
187
188 /* ──────────────────── Root component ──────────────────── */
189 export const MemoryGame = () => {
190 const [state, dispatch] = useReducer(reducer, undefined, initial);
191
192 /* Handle “flipBack” after 1 s for a mismatch */
193 useEffect(() => {
194 if (state.busy) {
195 const t = setTimeout(() => dispatch({ type: "flipBack" }), 1000);
196 return () => clearTimeout(t);
197 }
198 }, [state.busy]);
199
200 /* Quick win detection */
201 const solved = state.board.every((r) => r.every((c) => c.isSolved));
202
203 return (
204 <>
205 <h3 style={{ textAlign: "center" }}>
206 {solved ? "🎉 You won!" : "Memory Game"}
207 </h3>
208 <BoardContext.Provider value={dispatch}>
209 {state.board.map((row, idx) => (
210 <Row key={idx} rowPos={idx} row={row} />
211 ))}
212 </BoardContext.Provider>
213
214 <div style={{ textAlign: "center", marginTop: 12 }}>
215 <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
216 </div>
217 </>
218 );
219 };
220