Skip to content

Commit 4683b6f

Browse files
committed
refactor(galaga): fix multiple swap! calls per Erik's feedback
Apply Erik's atomic state update pattern to Galaga game, fixing 3 instances of multiple swap! calls that could cause race conditions and unnecessary re-renders. Fixes: 1. Bullet-Enemy Collision: Combined 5 separate swap! calls into single atomic update (bullets, enemies, score, particles) 2. Enemy Bullet-Player Collision: Combined 5 separate swap! calls into single atomic update (bullets, lives, particles, game status, high score) 3. Formation Movement: Eliminated nested swap! inside swap! function, replaced with single swap! using cond logic for direction changes Benefits: - No race conditions in multi-threaded environments - Single Reagent re-render per game event instead of multiple - Cleaner code using threading macros - Predictable atomic state updates - Follows functional programming best practices Pattern used: Single swap! with update function and threading (->) for all related state changes, with conditional logic for optional updates.
1 parent 0e673ae commit 4683b6f

1 file changed

Lines changed: 77 additions & 47 deletions

File tree

src/scittle/games/galaga.cljs

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -322,18 +322,25 @@
322322
(assoc star :y new-y))))
323323
stars))))
324324
;; Update formation movement (side-to-side only, no descent)
325-
(let [direction (:formation-direction @game-state)]
326-
(swap! game-state update :formation-offset
327-
(fn [{:keys [x y]}]
328-
(let [new-x (+ x (* direction 0.5))]
329-
(cond
330-
(and (>= new-x 30) (= direction 1))
331-
(do (swap! game-state assoc :formation-direction -1)
332-
{:x 30 :y y}) ; Keep y the same - no descent!
333-
(and (<= new-x -30) (= direction -1))
334-
(do (swap! game-state assoc :formation-direction 1)
335-
{:x -30 :y y}) ; Keep y the same - no descent!
336-
:else {:x new-x :y y})))))
325+
(let [direction (:formation-direction @game-state)
326+
offset (:formation-offset @game-state)
327+
x (:x offset)
328+
y (:y offset)
329+
new-x (+ x (* direction 0.5))]
330+
;; Single atomic update for formation movement and direction changes
331+
(swap! game-state
332+
(fn [state]
333+
(cond
334+
(and (>= new-x 30) (= direction 1))
335+
(-> state
336+
(assoc :formation-direction -1)
337+
(assoc :formation-offset {:x 30 :y y}))
338+
(and (<= new-x -30) (= direction -1))
339+
(-> state
340+
(assoc :formation-direction 1)
341+
(assoc :formation-offset {:x -30 :y y}))
342+
:else
343+
(assoc state :formation-offset {:x new-x :y y})))))
337344
;; Update enemies
338345
(swap! game-state update :enemies
339346
(fn [enemies]
@@ -380,45 +387,68 @@
380387
enemy enemies]
381388
(when (and (collides? :a bullet :b enemy)
382389
(not= (:state enemy) :destroyed))
383-
;; Remove bullet
384-
(swap! game-state update :bullets
385-
#(vec (remove (fn [b] (= b bullet)) %)))
386-
;; Damage enemy
387-
(let [new-hits (dec (:hits enemy))]
388-
(if (<= new-hits 0)
389-
;; Destroy enemy
390-
(do
391-
(play-explosion-sound) ; Play explosion sound
392-
(swap! game-state update :enemies
393-
#(vec (remove (fn [e] (= e enemy)) %)))
394-
(swap! game-state update :score + (:points enemy))
395-
(swap! game-state update :particles
396-
#(vec (concat % (create-particles :x (:x enemy)
397-
:y (:y enemy)
398-
:count 10
399-
:color (:color enemy))))))
400-
;; Damage enemy
401-
(swap! game-state update :enemies
402-
#(mapv (fn [e]
403-
(if (= e enemy)
404-
(assoc e :hits new-hits)
405-
e))
406-
%))))))
390+
(let [new-hits (dec (:hits enemy))
391+
destroyed? (<= new-hits 0)]
392+
(when destroyed?
393+
(play-explosion-sound)) ; Play explosion sound
394+
;; Single atomic update for all collision effects
395+
(swap! game-state
396+
(fn [state]
397+
(-> state
398+
;; Remove bullet
399+
(update :bullets #(vec (remove (fn [b] (= b bullet)) %)))
400+
;; Handle enemy destruction or damage
401+
(update :enemies
402+
(fn [enemies]
403+
(if destroyed?
404+
;; Destroy enemy
405+
(vec (remove (fn [e] (= e enemy)) enemies))
406+
;; Damage enemy
407+
(mapv (fn [e]
408+
(if (= e enemy)
409+
(assoc e :hits new-hits)
410+
e))
411+
enemies))))
412+
;; Add score if destroyed
413+
(#(if destroyed?
414+
(update % :score + (:points enemy))
415+
%))
416+
;; Add particles if destroyed
417+
(#(if destroyed?
418+
(update % :particles
419+
(fn [particles]
420+
(vec (concat particles
421+
(create-particles :x (:x enemy)
422+
:y (:y enemy)
423+
:count 10
424+
:color (:color enemy))))))
425+
%))))))))
407426
;; Check enemy bullet-player collisions
408427
(doseq [bullet enemy-bullets]
409428
(when (collides? :a bullet :b player)
410429
(play-hit-sound) ; Play hit sound
411-
(swap! game-state update :enemy-bullets
412-
#(vec (remove (fn [b] (= b bullet)) %)))
413-
(swap! game-state update-in [:player :lives] dec)
414-
(swap! game-state update :particles
415-
#(vec (concat % (create-particles :x (:x player)
416-
:y (:y player)
417-
:count 15
418-
:color "#FFFF00"))))
419-
(when (<= (get-in @game-state [:player :lives]) 0)
420-
(swap! game-state assoc :game-status :game-over)
421-
(swap! game-state update :high-score max (:score @game-state)))))
430+
;; Single atomic update for all hit effects
431+
(let [new-lives (dec (:lives player))
432+
game-over? (<= new-lives 0)]
433+
(swap! game-state
434+
(fn [state]
435+
(-> state
436+
;; Remove bullet
437+
(update :enemy-bullets #(vec (remove (fn [b] (= b bullet)) %)))
438+
;; Decrement lives
439+
(update-in [:player :lives] dec)
440+
;; Add particles
441+
(update :particles
442+
#(vec (concat % (create-particles :x (:x player)
443+
:y (:y player)
444+
:count 15
445+
:color "#FFFF00"))))
446+
;; If game over, update status and high score
447+
(#(if game-over?
448+
(-> %
449+
(assoc :game-status :game-over)
450+
(update :high-score max (:score %)))
451+
%))))))))
422452
;; Check for wave clear
423453
(when (empty? enemies)
424454
(play-wave-complete-sound) ; Play victory sound

0 commit comments

Comments
 (0)