Skip to content

Commit ddc0dd7

Browse files
authored
Merge pull request #133 from burinc/memory-game
Add memory game article and improve series navigation
2 parents 76ec4af + 8ea4d7b commit ddc0dd7

File tree

10 files changed

+732
-0
lines changed

10 files changed

+732
-0
lines changed

src/scittle/games/memory-game.png

166 KB
Loading

src/scittle/games/memory_game.cljs

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
(ns scittle.games.memory-game
2+
(:require [reagent.core :as r]
3+
[reagent.dom :as rdom]))
4+
5+
;; ============================================================================
6+
;; Utility Styles
7+
;; ============================================================================
8+
9+
(defn merge-styles
10+
"Safely merges multiple style maps"
11+
[& styles]
12+
(apply merge (filter map? styles)))
13+
14+
;; ============================================================================
15+
;; Game State
16+
;; ============================================================================
17+
18+
(def game-state (r/atom {:sequence [] ; Computer's sequence
19+
:player-sequence [] ; Player's current input
20+
:playing? false ; Is game active?
21+
:showing? false ; Is computer showing sequence?
22+
:score 0 ; Current score/level
23+
:game-over? false ; Game over state
24+
:active-tile nil ; Currently lit up tile
25+
:high-score 0})) ; Best score achieved
26+
27+
;; ============================================================================
28+
;; Game Configuration
29+
;; ============================================================================
30+
31+
(def game-colors ["#4caf50" ; Green
32+
"#ff4444" ; Red
33+
"#ff9800" ; Orange
34+
"#2196f3"]) ; Blue
35+
36+
;; Musical notes (frequencies in Hz) for each color
37+
(def color-frequencies [261.63 ; C4 - Green
38+
329.63 ; E4 - Red
39+
392.00 ; G4 - Orange
40+
523.25]) ; C5 - Blue
41+
42+
;; ============================================================================
43+
;; Audio Functions
44+
;; ============================================================================
45+
46+
(def audio-context (when (exists? js/AudioContext) (js/AudioContext.)))
47+
48+
(defn play-tone
49+
"Plays a tone at the specified frequency for the given duration"
50+
[& {:keys [frequency duration]
51+
:or {frequency 440 duration 0.2}}]
52+
(when audio-context
53+
(try
54+
(let [oscillator (.createOscillator audio-context)
55+
gain-node (.createGain audio-context)]
56+
(.connect oscillator gain-node)
57+
(.connect gain-node (.-destination audio-context))
58+
;; Set frequency and gain values
59+
(set! (.-value (.-frequency oscillator)) frequency)
60+
(set! (.-value (.-gain gain-node)) 0.3)
61+
(.start oscillator)
62+
(.stop oscillator (+ (.-currentTime audio-context) duration)))
63+
(catch js/Error e
64+
(js/console.error "Audio error:" e)))))
65+
66+
;; ============================================================================
67+
;; Game Logic
68+
;; ============================================================================
69+
70+
(defn add-to-sequence
71+
"Adds a random color to the game sequence"
72+
[]
73+
(let [next-color (rand-int 4)]
74+
(swap! game-state update :sequence conj next-color)))
75+
76+
(defn show-sequence
77+
"Shows the current sequence to the player with visual and audio feedback"
78+
[]
79+
(swap! game-state assoc
80+
:showing? true
81+
:player-sequence [])
82+
(let [sequence (:sequence @game-state)
83+
show-duration 600 ; ms between each color
84+
display-duration 400] ; ms to display each color
85+
;; Show each color in sequence
86+
(doseq [[idx color-idx] (map-indexed vector sequence)]
87+
(js/setTimeout
88+
(fn []
89+
;; Light up the tile and play sound
90+
(swap! game-state assoc :active-tile color-idx)
91+
(play-tone :frequency (nth color-frequencies color-idx)
92+
:duration 0.5)
93+
;; Turn off the tile after display duration
94+
(js/setTimeout
95+
#(swap! game-state assoc :active-tile nil)
96+
display-duration))
97+
(* idx show-duration)))
98+
;; Enable player input after showing complete sequence
99+
(js/setTimeout
100+
#(swap! game-state assoc :showing? false)
101+
(* (count sequence) show-duration))))
102+
103+
(defn handle-tile-click
104+
"Handles player clicking a tile"
105+
[& {:keys [tile-index]}]
106+
(when (and (:playing? @game-state)
107+
(not (:showing? @game-state))
108+
(not (:game-over? @game-state)))
109+
;; Play sound and show visual feedback
110+
(play-tone :frequency (nth color-frequencies tile-index)
111+
:duration 0.2)
112+
(swap! game-state assoc :active-tile tile-index)
113+
(js/setTimeout #(swap! game-state assoc :active-tile nil) 200)
114+
115+
;; Add to player sequence
116+
(swap! game-state update :player-sequence conj tile-index)
117+
118+
(let [{:keys [sequence player-sequence score high-score]} @game-state
119+
current-position (dec (count player-sequence))]
120+
(cond
121+
;; Wrong input - game over
122+
(not= (nth player-sequence current-position)
123+
(nth sequence current-position))
124+
(do
125+
(swap! game-state assoc
126+
:game-over? true
127+
:playing? false
128+
:high-score (max score high-score))
129+
;; Play error sound
130+
(play-tone :frequency 100 :duration 0.5))
131+
132+
;; Correct and sequence complete - next level
133+
(= (count player-sequence) (count sequence))
134+
(do
135+
(swap! game-state update :score inc)
136+
(js/setTimeout
137+
(fn []
138+
(add-to-sequence)
139+
(show-sequence))
140+
1000))))))
141+
142+
(defn start-game
143+
"Starts a new game"
144+
[]
145+
(reset! game-state (merge @game-state
146+
{:sequence []
147+
:player-sequence []
148+
:playing? true
149+
:showing? false
150+
:score 0
151+
:game-over? false
152+
:active-tile nil}))
153+
;; Add first color and show it after a short delay
154+
(add-to-sequence)
155+
(js/setTimeout show-sequence 500))
156+
157+
;; ============================================================================
158+
;; UI Components
159+
;; ============================================================================
160+
161+
(defn color-tile
162+
"Renders a clickable color tile"
163+
[& {:keys [color index on-click disabled?]}]
164+
(let [active? (= (:active-tile @game-state) index)]
165+
[:div {:style (merge-styles
166+
{:width "100px"
167+
:height "100px"
168+
:background-color color
169+
:border-radius "10px"
170+
:cursor (if disabled? "not-allowed" "pointer")
171+
:opacity (cond
172+
active? 1
173+
disabled? 0.5
174+
:else 0.8)
175+
:transform (if active? "scale(1.1)" "scale(1)")
176+
:box-shadow (if active?
177+
"0 0 20px rgba(255,255,255,0.8)"
178+
"0 2px 4px rgba(0,0,0,0.2)")
179+
:transition "all 0.2s ease"})
180+
:on-click (when (and on-click (not disabled?))
181+
#(on-click :tile-index index))
182+
:on-mouse-enter (fn [e]
183+
(when (and (not disabled?) (not active?))
184+
(set! (.. e -target -style -transform) "scale(1.05)")
185+
(set! (.. e -target -style -opacity) "1")
186+
(set! (.. e -target -style -boxShadow)
187+
"0 4px 8px rgba(0,0,0,0.3)")))
188+
:on-mouse-leave (fn [e]
189+
(when (and (not disabled?) (not active?))
190+
(set! (.. e -target -style -transform) "scale(1)")
191+
(set! (.. e -target -style -opacity) "0.8")
192+
(set! (.. e -target -style -boxShadow)
193+
"0 2px 4px rgba(0,0,0,0.2)")))}]))
194+
195+
(defn game-controls
196+
"Game control buttons"
197+
[& {:keys [playing? game-over? on-start on-give-up]}]
198+
[:div {:style {:display "flex"
199+
:gap "10px"
200+
:justify-content "center"}}
201+
(if (not playing?)
202+
[:button {:style {:padding "10px 20px"
203+
:background "#4caf50"
204+
:color "white"
205+
:font-size "16px"
206+
:border "none"
207+
:border-radius "4px"
208+
:cursor "pointer"
209+
:transition "all 0.2s ease"}
210+
:on-click on-start
211+
:on-mouse-enter #(set! (.. % -target -style -background) "#45a049")
212+
:on-mouse-leave #(set! (.. % -target -style -background) "#4caf50")}
213+
(if game-over? "Try Again" "Start Game")]
214+
[:button {:style {:padding "10px 20px"
215+
:background "#ff4444"
216+
:color "white"
217+
:font-size "16px"
218+
:border "none"
219+
:border-radius "4px"
220+
:cursor "pointer"
221+
:transition "all 0.2s ease"}
222+
:on-click on-give-up
223+
:on-mouse-enter #(set! (.. % -target -style -background) "#ff3333")
224+
:on-mouse-leave #(set! (.. % -target -style -background) "#ff4444")}
225+
"Give Up"])])
226+
227+
(defn score-display
228+
"Displays the current score or game over message"
229+
[& {:keys [playing? game-over? score high-score]}]
230+
[:div
231+
[:h3 {:style {:margin-bottom "20px"
232+
:font-size "24px"
233+
:color (cond
234+
game-over? "#ff4444"
235+
playing? "#4caf50"
236+
:else "#333")}}
237+
(cond
238+
game-over? (str "Game Over! Final Score: " score)
239+
playing? (str "Level: " (inc score))
240+
:else "Press Start to Play")]
241+
242+
;; High score display
243+
(when (and (> high-score 0) (not playing?))
244+
[:div {:style {:margin-top "10px"
245+
:padding "8px 16px"
246+
:background "#f0f0f0"
247+
:border-radius "20px"
248+
:display "inline-block"
249+
:color "#666"
250+
:font-size "14px"}}
251+
(str "🏆 Best: " high-score)])])
252+
253+
(defn status-indicator
254+
"Shows the current game status"
255+
[& {:keys [playing? showing? player-sequence sequence]}]
256+
(when playing?
257+
[:div {:style {:margin-bottom "15px"
258+
:height "20px"
259+
:color "#666"
260+
:font-size "14px"}}
261+
(cond
262+
showing? "Watch carefully..."
263+
(empty? player-sequence) "Your turn!"
264+
:else (str "Progress: " (count player-sequence) "/" (count sequence)))]))
265+
266+
(defn game-tips
267+
"Displays helpful game tips"
268+
[]
269+
[:div {:style {:margin-top "30px"
270+
:padding "15px"
271+
:background "#f9f9f9"
272+
:border-radius "8px"
273+
:text-align "left"}}
274+
[:h4 {:style {:margin-bottom "10px"
275+
:color "#4caf50"}}
276+
"How to Play:"]
277+
[:ul {:style {:margin "0"
278+
:padding-left "20px"
279+
:color "#666"
280+
:font-size "14px"
281+
:line-height "1.6"}}
282+
[:li "Watch the sequence of colors light up"]
283+
[:li "Click the tiles in the same order"]
284+
[:li "Each round adds one more color"]
285+
[:li "How many can you remember?"]]])
286+
287+
;; ============================================================================
288+
;; Main Component
289+
;; ============================================================================
290+
291+
(defn memory-game
292+
"Main memory game component"
293+
[]
294+
(let [{:keys [sequence player-sequence playing? showing?
295+
score game-over? high-score]} @game-state]
296+
[:div {:style {:padding "20px"
297+
:max-width "600px"
298+
:margin "0 auto"
299+
:font-family "system-ui, -apple-system, sans-serif"}}
300+
301+
;; Title
302+
[:h2 {:style {:text-align "center"
303+
:color "#4caf50"
304+
:margin-bottom "20px"}}
305+
"🧠 Memory Game"]
306+
307+
[:div {:style {:padding "30px"
308+
:background "white"
309+
:border-radius "8px"
310+
:box-shadow "0 2px 8px rgba(0, 0, 0, 0.1)"
311+
:text-align "center"}}
312+
313+
;; Instructions
314+
[:p {:style {:margin-bottom "20px"
315+
:color "#666"
316+
:font-size "14px"}}
317+
"Watch the sequence and repeat it back!"]
318+
319+
;; Score display
320+
[score-display :playing? playing?
321+
:game-over? game-over?
322+
:score score
323+
:high-score high-score]
324+
325+
;; Status indicator
326+
[status-indicator :playing? playing?
327+
:showing? showing?
328+
:player-sequence player-sequence
329+
:sequence sequence]
330+
331+
;; Game tiles
332+
[:div {:style {:display "flex"
333+
:gap "10px"
334+
:justify-content "center"
335+
:margin-bottom "30px"
336+
:flex-wrap "wrap"}}
337+
(map-indexed
338+
(fn [idx color]
339+
^{:key idx}
340+
[color-tile :color color
341+
:index idx
342+
:on-click handle-tile-click
343+
:disabled? (or showing? (not playing?))])
344+
game-colors)]
345+
346+
;; Control buttons
347+
[game-controls :playing? playing?
348+
:game-over? game-over?
349+
:on-start start-game
350+
:on-give-up (fn []
351+
(swap! game-state assoc
352+
:playing? false
353+
:game-over? true))]
354+
355+
;; Game tips
356+
[game-tips]]]))
357+
358+
;; Export the main component
359+
(def page memory-game)
360+
361+
;; ============================================================================
362+
;; Mount point
363+
;; ============================================================================
364+
365+
(defn ^:export mount-memory-game
366+
"Mount the memory game component to the DOM"
367+
[]
368+
(when-let [el (js/document.getElementById "memory-game-root")]
369+
(rdom/render [page] el)))
370+
371+
;; Auto-mount when script loads
372+
(mount-memory-game)

0 commit comments

Comments
 (0)