3
\$\begingroup\$

This is the initial skeleton for a short game written in Fennel for a Jam event. It runs sensibly, but I want to know if I'm missing something that could make the code more elegant as it's a bit confusing and smelly at the moment.

Edit: since it seems people found this interesting, updated with the actual final release code. Feedback for future learning is still valuable.

(global cheat false)
(global SCREEN_WIDTH 400)
(global SCREEN_HEIGHT 240)
(global START_LEVEL 1)
(global GRID_SIZE 20)
(global GRID_WIDTH (/ SCREEN_WIDTH GRID_SIZE))
(global GRID_HEIGHT (- (/ SCREEN_HEIGHT GRID_SIZE) 1))
(global ANIMATION_SPEED 0.5)
(global BEAT_FREQ (/ 60 113))
(global MOVE_TIME BEAT_FREQ)
(global PLAYER_MOVE_TIME 0.1)
(global BASE_SEQUENCE [ 3 3 2 0 1 1 4 0 ])
(global DIRECTIONS [ {:dx -1 :dy 0} {:dx 0 :dy 1} {:dx 1 :dy 0} {:dx 0 :dy -1} ])
(global LEVELS [ {:tx 10 :ty 0 :seq [3 0 3 0 1 0 1 0] } 
                 {:tx 10 :ty 9 :seq [1 1 4 0 3 3 2 0] }
                 {:tx 0 :ty 5 :seq [0 3 3 2 4 2 4 1 1 0] }
                 {:tx 19 :ty 5 :seq [1 4 4 0 2 2 3 0] } ] )
(global GRID_BOTTOM (- SCREEN_HEIGHT GRID_SIZE))
(global BEATS_ON_RIBBON 8)
(global RIBBON_TIME_FACTOR (/ SCREEN_WIDTH (* BEAT_FREQ BEATS_ON_RIBBON)))
(global SYNC_TOLERANCE 0.1)
(global FAIL_HEIGHT 50)
(global FAIL_WIDTH 200)
(global FAIL_EFFECT_TIME 0.5)
(global font (love.graphics.newFont "futurabook.ttf" 35))
(global music (love.audio.newSource "etiq2pr.mp3" "stream"))

(require "math")
(require "os")

(macro update [x ...]
  `(set ,x (-> ,x ,...)))
  
(macro unless [x ...]
  `(when (not ,x) ,...))
  
(fn lightColor []
  (love.graphics.setColor 0.69 0.68 0.65 1))
  
(fn darkColor [] 
  (love.graphics.setColor 0.19 0.18 0.16 1))

(fn addListWrap [v d max]
  (let [r (+ v d)]
    (if (<= r max) r 1)))
    
(fn initNpcItem [x y sprite]
  {:x x
   :y y
   :dx 0
   :dy 0
   :idx 0
   :idy 0
   :moveTime 0
   :moveSpeed 0
   :frame (math.random 8)
   :frameTime 0
   :maxFrame (if (= sprite 1) 8 10)})
  

(fn reInit []
  ; Initialize player and basics.
  (global state {
     :items [ { :x 10 :y 9 :dx 0 :dy 0 :idx 0 :idy 0 :idir 0 :moveTime 0 :moveSpeed 0 :frame 1 :frameTime 0 } ]
     :beatTime 0
     :masterTime 0
     :seq (. (. LEVELS START_LEVEL) :seq)
     :seqStep 1 
     :pendingControl 0
     :leadIn 4
     :failed false
     :aniTime 0
     :sillyAngle 0
     :failEffect 0
     :level START_LEVEL
     :message (love.graphics.newText font "Ready?")})
    
  (when (> START_LEVEL 1) 
    (let [p (. state.items 1)
          lastLevel (- START_LEVEL 1)]
      (set p.x (. (. LEVELS lastLevel) :tx))
      (set p.y (. (. LEVELS lastLevel) :ty))))
      
    
  ; Choose sprites at random or follow selected direction.
  (if
    (love.keyboard.isDown "left") (do (set state.npcsprite 1) (set state.pcsprite 2))
    (love.keyboard.isDown "right") (do (set state.npcsprite 2) (set state.pcsprite 1))
    (love.keyboard.isDown "up") (do (set state.npcsprite 1) (set state.pcsprite 1))
    (love.keyboard.isDown "down") (do (set state.npcsprite 2) (set state.pcsprite 2))
    (do
      (set state.npcsprite (math.random 2))
      (set state.pcsprite (if (= state.npcsprite 2) 1 2))))
  
  ;Load NPCs    
  (for [ny 1 9 3]   
    (for [nx 3 16 6]
      (table.insert state.items (initNpcItem nx ny state.npcsprite))))
      
  ;Start music
  (music:seek 0)
  (music:setPitch 1)
  (love.audio.play music))


(fn curlevel []
  (. LEVELS state.level))


  
;; First time setup of everything.
(fn love.load [arg]
  ;Set up mode and RNG
  (love.window.setMode SCREEN_WIDTH SCREEN_HEIGHT {:vsync 1})
  (love.window.setTitle "Crossing the Floor")
  (love.graphics.setBackgroundColor 0.69 0.68 0.65 1)
  (math.randomseed (os.time))
  
  ;Load sprites
  (global frames [ [] [] ])
  (for [x 1 8]
    (table.insert (. frames 1) (love.graphics.newImage (.. "dn1_" x ".png"))))
  (for [x 1 10]
    (table.insert (. frames 2) (love.graphics.newImage (.. "dn2_" x ".png"))))
  
  ;Load audio
  (global audio {
     :tick (love.audio.newSource "metrotick.wav" "static")})
  (global arrows {
     :left (love.graphics.newImage "arrowlt.png")
     :right (love.graphics.newImage "arrowrt.png")
     :up (love.graphics.newImage "arrowup.png")
     :down (love.graphics.newImage "arrowdn.png")})
  (global arrowIndexes [ arrows.left arrows.down arrows.right arrows.up ])
  (music:setLooping true)
  
  ;Initialize game, but then go straight to dormant state (kludgey)
  (reInit)
  (set state.failed true)
  (set state.failEffect FAIL_EFFECT_TIME)
  (music:stop)
  (set state.message (love.graphics.newText font "Press Space")))

;; Called when player fails
(fn setFail [reason]
  (unless state.failed
    (set state.failed true)
    (set state.failEffect 0)
    (set state.message (love.graphics.newText font reason))))

;; Actually perform a move
(fn startItemMove [i dx dy pl]
  (set i.dx dx)
  (set i.dy dy)
  (update i.x (+ dx))
  (update i.y (+ dy))
  (set i.moveTime (if (= pl 1) PLAYER_MOVE_TIME MOVE_TIME))
  (set i.moveSpeed (/ GRID_SIZE i.moveTime)))
      
;; Set intended move
(fn intendItemMove [i dx dy idir]
  (set i.idx dx)
  (set i.idy dy)
  (set i.idir idir))


(fn intendedDest [i]
  {:x (+ i.x i.idx) :y (+ i.y i.idy)})
  
;; Resolve moves based on listed intentions.
(fn resolveIntendedMoves []
  (var ok false)
  
  ;; Bounds checks
  (for [x 1 (length state.items)]
    (set ok true)
    (let [i (. state.items x)
          ip (intendedDest i)]
     (if
       (< ip.x 0) (set ok false)
       (>= ip.x GRID_WIDTH) (set ok false)
       (< ip.y 0) (set ok false)
       (>= ip.y GRID_HEIGHT) (set ok false))
     (unless ok
       (intendItemMove i 0 0 0)
       (when (= x 1) (setFail "Floor Out!")))))
       
  ;; Check intended moves for collisions.
  (for [x 1 (length state.items)]
    (set ok true)
    (let [i (. state.items x)
          ip (intendedDest i)]
      (for [y (+ 1 x) (length state.items)]
        (let [j (. state.items y)
              jp (intendedDest j)]
          (when (and (= ip.x jp.x) (= ip.y jp.y))
            (set ok false)
            (intendItemMove j 0 0 0))))
      (unless ok
        (intendItemMove i 0 0 0)
        (when (= x 1) (setFail "Crashed!")))))
    
  ;; Make moves that weren't eliminated above.
  (for [x 1 (length state.items)]
    (let [i (. state.items x)]
      (when (or (~= 0 i.idx) (~= 0 i.idy))
        (startItemMove i i.idx i.idy x)
        (intendItemMove i 0 0 0)))))
      
(fn advanceLevel []
  (update state.level (+ 1))
  (if (> state.level (length LEVELS))
    (do
      (set state.level 1)
      (set state.failed true)
      (set state.failEffect FAIL_EFFECT_TIME)
      (set state.message (love.graphics.newText font (.. "Done! " (math.ceil (* state.masterTime 10))))))
    (do      
      (set state.seqStep 1)
      (set state.seq (. (curlevel) :seq))
      (set state.leadIn 4)
      (set state.message (love.graphics.newText font "Change!")))))
      
(fn beatMoveItems []
  ;; Convert pending control to intended move.
  (when (not (or state.failed (> state.leadIn 0)))
    (let [p (. state.items 1)]
      (if
        (= 1 state.pendingControl) (intendItemMove p -1 0 1) 
        (= 2 state.pendingControl) (intendItemMove p 0 1 2) 
        (= 3 state.pendingControl) (intendItemMove p 1 0 3) 
        (= 4 state.pendingControl) (intendItemMove p 0 -1 4))))
        
  ;; If there's a step this beat..
  (let [st (. state.seq state.seqStep)]
    (when (> st 0)
    
      ;; Fail player if pending control didn't match
      (when (and (~= state.pendingControl st) (= 0 state.leadIn) (not cheat))
        (setFail (if (= state.pendingControl 0) "Missed it!" "Wrong way!")))
        
      ;; Move all NPCs based on step
      (for [x 2 (length state.items)]
        (let [i (. state.items x)
              dst (. DIRECTIONS st)]
          (intendItemMove i dst.dx dst.dy st)))))
           
  ;; Clear pending control and advance step
  (set state.pendingControl 0)
  (update state.seqStep (addListWrap 1 (length state.seq)))
  (resolveIntendedMoves))
      
      
(fn love.update [dt]
  (update state.beatTime (+ dt))
  (update state.masterTime (+ dt))
  (update state.aniTime (+ dt))
  (when (and state.failed (< state.failEffect FAIL_EFFECT_TIME))
    (update state.failEffect (+ dt))
    (if (>= state.failEffect FAIL_EFFECT_TIME)
      (music:stop)
      (music:setPitch (- 1 (/ state.failEffect FAIL_EFFECT_TIME)))))
  (unless state.failed 
    (if
      (love.keyboard.isDown "left") (set state.pendingControl 1)
      (love.keyboard.isDown "down") (set state.pendingControl 2)
      (love.keyboard.isDown "right") (set state.pendingControl 3)
      (love.keyboard.isDown "up") (set state.pendingControl 4)))
  (when (and state.failed (love.keyboard.isDown "space")) 
    (reInit))
  (let [musicTime (music:tell)]
    (when (> (math.abs (- musicTime state.masterTime)) SYNC_TOLERANCE)
      (music:seek state.masterTime)))
  (when (>= state.beatTime BEAT_FREQ)
    (update state.beatTime (- BEAT_FREQ))
    (when (and (> state.leadIn 0) (not state.failed))
      (update state.leadIn (- 1))
      (love.audio.play audio.tick)
      (set state.message (love.graphics.newText font (tostring state.leadIn))))     
    (beatMoveItems))
  (let [p (. state.items 1)]
    (when (and (= p.x (. (curlevel) :tx)) (= p.y (. (curlevel) :ty)) (= p.moveTime 0))
      (advanceLevel)))

  (let [doAni (>= state.aniTime ANIMATION_SPEED)]   
    (for [x 1 (length state.items)]
        (let [i (. state.items x)]
            (when (> i.moveTime 0)
                (update i.moveTime (- dt))
                (when (<= i.moveTime 0)
                    (set i.moveTime 0)
                    (set i.dx 0)
                    (set i.dy 0)))
            (when doAni
              (if (= x 1)
                (update i.frame (addListWrap 1 (if (= state.pcsprite 2) 10 8)))
                (update i.frame (addListWrap 1 (if (= state.npcsprite 2) 10 8)))))))
    (when doAni (update state.aniTime (- ANIMATION_SPEED)))))
  

 
(fn drawGrid []
   (for [x 0 SCREEN_WIDTH GRID_SIZE]
     (love.graphics.line x 0 x GRID_BOTTOM))
   (for [y 0 GRID_BOTTOM GRID_SIZE]
     (love.graphics.line 0 y SCREEN_WIDTH y))
   (let [tx (* GRID_SIZE (. (curlevel) :tx))
         ty (* GRID_SIZE (. (curlevel) :ty))]
     (love.graphics.line tx ty (+ tx GRID_SIZE) (+ ty GRID_SIZE))
     (love.graphics.line tx (+ ty GRID_SIZE) (+ tx GRID_SIZE) ty)))
     
     
(fn drawItems []
  (love.graphics.setColor 1 1 1 1) ; Without this, images are tinted
  (for [x 1 (length state.items)]
    (let [i (. state.items x)
          mi (* i.moveTime i.moveSpeed)
          realX (- (* i.x GRID_SIZE) (* i.dx mi))
          realY (- (* i.y GRID_SIZE) (* i.dy mi))
          goX (+ realX (/ GRID_SIZE 2))
          goY (+ realY (/ GRID_SIZE 2))]
      (love.graphics.translate goX goY)
      (love.graphics.rotate state.sillyAngle)
      (love.graphics.translate (- 0 goX) (- 0 goY))
      (if (= x 1)
          (if (= state.pendingControl 0)
            (love.graphics.draw (. (. frames state.pcsprite) i.frame) realX realY)
            (do (love.graphics.origin) (love.graphics.draw (. arrowIndexes state.pendingControl) realX realY)))
          (love.graphics.draw (. (. frames state.npcsprite) i.frame) realX realY)))
      (love.graphics.origin)))

    
(fn drawRibbon []
  (darkColor)
  (let [midLineX (/ (* (/ BEAT_FREQ 2) RIBBON_TIME_FACTOR) 2)]
    (love.graphics.line midLineX GRID_BOTTOM midLineX SCREEN_HEIGHT))
  (love.graphics.setColor 1 1 1 1) ; Without this, images are tinted
  (var rSeqStep state.seqStep)
  (unless (= state.pendingControl 0)
    (love.graphics.draw (. arrowIndexes state.pendingControl) 0 GRID_BOTTOM))
  (for [x 1 BEATS_ON_RIBBON]
    (let [beatStart (* x (* BEAT_FREQ RIBBON_TIME_FACTOR))
          beatOffset (* state.beatTime RIBBON_TIME_FACTOR)
          beatX (- beatStart beatOffset)]
      (if (> (. state.seq rSeqStep) 0)
        (do (love.graphics.setColor 1 1 1 1) (love.graphics.draw (. arrowIndexes (. state.seq rSeqStep))
                            beatX GRID_BOTTOM))
        (do (darkColor) (love.graphics.line beatX (+ 10 GRID_BOTTOM) (+ 20 beatX) (+ 10 GRID_BOTTOM)))))
    (update rSeqStep (addListWrap 1 (length state.seq)))))

                        
(fn drawMessage [] 
  (lightColor)
  (let [fx (- (/ SCREEN_WIDTH 2) (/ FAIL_WIDTH 2))
        fy (- (/ SCREEN_HEIGHT 2) (/ FAIL_HEIGHT 2))]
    (love.graphics.rectangle "fill" fx fy FAIL_WIDTH FAIL_HEIGHT)
    (darkColor)
    (love.graphics.setLineWidth 3)
    (love.graphics.rectangle "line" fx fy FAIL_WIDTH FAIL_HEIGHT)
    (love.graphics.setLineWidth 1)
    (love.graphics.draw state.message (- (/ SCREEN_WIDTH 2) (/ (state.message:getWidth) 2)) (+ 3 fy))))
    
    
(fn love.wheelmoved [x y]
  (update state.sillyAngle (- (* math.pi 0.1 y))))
  
                        
(fn love.draw []
   (darkColor)
   (drawGrid)
   (drawItems)
   (drawRibbon)
   (when (or state.failed (> state.leadIn 0)) (drawMessage)))
   
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.