The Arcology Garden

Group B Pace Notes

LifeTechEmacsArcology

I've been thinking about a rally card-game, either a 52-card deck or a custom art deck, called Group B or pacenotes or something, two players, a co-driver will draw a hand of cards and play them with a driver, who will react to the pacenotes, managing speed, turbo, reaction energy and try to get to the end of a stage.

or maybe it's a single player roguelite deckbuilder... Let's try that first:

2024 Autumn Lisp Game Jam

Experience the pure grit of the powerful Group B rally cars as you snake your way through ten treacherous stages at the Autumn Rally Classic!

this is a work in progress prototype of Group B, a single ten stage rally, the Autumn Rally Classic:

Balance focus, speed, car damage and tire health as you speed through fast flowing corners, tight hairpins, and perilous jumps.

Group B is a card game set in the world of rally racing; when you start you have four cards in your hand on the bottom left, and can select between them using the up and down keys or left mouse button; Press RET or right click or (A) if you have a gamepad to play a card.

On the top you'll see your core statistics and underneath that your progress through the stage, and each page of your co-driver's pace notes, these notes describe the challenges which you must resolve to survive the Autumn Rally Classic.

A brief view of the rules:

  • Each turn progresses you through the stage, and a rally is ten stages long:

    • Progress through the stage from left to right, the note visible above the car is your current challenge, you also have "visibility" up to two corners ahead thanks to the co-driver.

    • Resolve the pace note above the car using the cards in hand below; the cards' effects are described on the right. For example, you think you can go faster through the corner you floor it! if you think you're coming up to a turn and need to slow down you can do that or yank the handbrake.

    • Use your co-driver's pace notes to plan ahead and make sure you brake early, maximize speed, and effectively use your resources.

  • Flooring it on the straight portions of a stage will raise your speed up to that zone's value.

  • Turns must be completed at a speed less than or equal to the note's value or you will take damage.

  • You can play a corner-cut card to gain a huge advantage on corners marked with the corner-cut badge at the risk of crashing out hard. Go big, or go home? (You can also play them when you are going slow enough to progress through a corner as a discard.)

  • Taking dangerous jumps at speed risks damage to suspension.

  • If you want to get to the end of the rally be sure to carefully consider how you spend your time in service park.

  • Getting locked in throws you in to a fugue state; you'll find yourself past the current stage note going the speed of the next stage note, but you might have damaged the car getting there! Good luck utilizing this well.

  • The more speed you have, the higher your score, but if you're going too fast you'll wreck out in no time! Remember what they say about effective cornering: slow to enter, fast to exit!

This game was developed in 10 days using TIC-80 on the Planet Computer Cosmo Communicator, a neat little android device I wish I could recommend to you. It was nice to write this game on the couch, on the bus, and in the forests of Lane County, OR , in five or fifty minute intervals throughout the week and change. The last step of the development of the prototype was to turn it in to this Literate Programming document which has the compiled object embedded above and the source code which produces it embedded below.

What's Rally Racing?

Rally Racing is a form of Motorsport that takes place on public roads with street-legal vehicles. It is a grueling endurance test where a driver is led by a co-driver through those public roads as quickly as possible reading from a set of directions prepared ahead of time.

Here is an example of how frenetic this can feel in the car:

"Street-legal" and "public roads" do a lot of heavy lifting as rallying became associate with 700 horsepower turbo-charged monsters going 140mph through narrow forest gravel roads. These are a far cry from any Toyota Yaris or Hyundai i20 hatchback you'd see at your grocer.

The sport began in the late 60s with a tradition of European aristocrats driving their sports cars to Monaco as fast as they can to meet up together regardless of where they started and developed in to a sport taking participants on fast narrow roads between villages around the world. The Autumn Rally Classic takes place in that professionalized Rallying boom during the 1970s:

Rally racing went through a period of famous expansion and famous heartbreak as lighter cars, bigger engines, and larger crowds met the reality of bad brakes and the 80s' cavalier attitudes towards life and well-being in general.

The 90s brought back production-based cars, the sorts of Subaru and Mitsubishi AWD turbocharged rattlecans that are still fairly common on the streets of Eugene, and yet again today the WRC is rallying cars that are space-frame carbon-monocoque prototype beasts generating more downforce at speed than a hydraulic press. Good people still are injured or perish today, but the siren call continues.

You can still go modify a used car or go to a dealer and convince them to order a Rally5 for you and take it out rallying yourself today if you want. Do make sure the roads closed and marshaled if you wanna go fast!

A Racing Card Game?

Motorsport is more than The Fast and the Furious let on, it's not like you can just keep looking longingly out your side window at your rival and down-shifting to go yet faster, pop the nitro at the right moment to power past them at the finish. Motorsport is about resource management, it's about being there at the end to look longingly out the your window at your rival.

Some of your resources:

  • Momentum

  • Fuel

  • Tire life/grip/health

  • Lubricants and engine health

  • Suspension and springs and dampers

  • the gravel on the road itself

  • the driver and co-drivers' attention

  • the mechanics' attention (sometimes they're the same folks)

The timing aspect, so to speak, when to spend your actions, is critical of course, and what folks focus on when they're playing a racing game. That list of resources is abbreviated, it's often about driving a perfect lap for four or five laps in a row while you pass CPU drivers.

If we discard the timing and "twitch skill" aspect and draw it down to the raw resources, it's fun to model it as a sort of card game or resource management simulator. And thus was the idea of Group B Pace Notes, a race where anyone can win, and they can break for coffee along the way if they'd like to.

There's something ironic/fun about taking something as fast paced as car racing and boiling it down to an action economy and a roguelite deckbuilder with permadeath. Maybe some day I'll add an inter-rally progression/championship system and ship a whole ass game like this. Hope you enjoy it.

Source Code for the Autumn Rally Classic

Top matter and helper junk

State vars:

fennel source: :noweb-ref topvars
(var t 0) (var idx 1) (local pview 3) (var screen :title) (var trees []) (var stage []) (var debounce false)

Helper functions that don't rely on anything but these state vars:

fennel source: :noweb-ref helpers
(local fennel (require "fennel")) (macro sprint [msg x y c s] `(do (print ,msg (+ ,x 1) (+ ,y 1) ,s) (print ,msg ,x ,y ,c))) (fn clamp [value low high] (if (< value low) low (if (> value high) high value))) (fn guess-row [yoff y max] (let [idx2 (math.floor (- (/ (- y yoff) 8) 1))] idx2))
fennel source: :noweb-ref interface
(local sprites { :focus 1 :speed 2 :car-health 3 :tire-health 4 :road-horiz 17 :stage-end 18 :caution-sign 19 :up-arrow 20 :down-arrow 21 :floor-it 64 :slow-down 65 :handbrake 66 :corner-cut 67 :water-hazard 68 :locked-in 69 :score 18 ; moar sprites... :car1 96 :car2 98 :tree1 160 :tree2 162 :ahead 128 :left 130 :right 132 :caution 134 }) (fn bsprite [id x y flip rotate] (spr (. sprites id) x y 10 1 (or flip 0) (or rotate 0) 2 2)) (fn sprite [id x y flip rotate] (spr (. sprites id) x y 10 1 (or flip 0) (or rotate 0)))

Deck Actions

fennel source: :noweb-ref deck-actions
(var deck []) (local hand []) (local discard-pile []) (fn move-card [i from to] (let [card (table.remove from i)] (table.insert to card)) (values from to)) (fn shuffle [deck] "fischer yates, one hopes" (each [first fcard (pairs deck)] (let [second (math.random (length deck)) scard (. deck second)] (tset deck first scard) (tset deck second fcard))) deck) (fn re-shuffle [] "discard pile in to deck, shuffle deck" (each [i c (pairs discard-pile)] (move-card i discard-pile deck)) (shuffle deck)) (fn draw [num] (for [i 1 (or num 1)] (when (= 0 (length deck)) (re-shuffle)) (move-card 1 deck hand))) (fn discard [idx] (when (>= (length hand) (or idx 1)) (move-card idx hand discard-pile)))

Game/Player state

fennel source: :noweb-ref state
(local limits {:focus [0 15] :speed [0 10] :car [0 100] :tires [0 100]}) (fn mk-state [] (let [state {:focus 5 :speed 0 :car 100 :tires 100 :score 0 :svc-time 45 :note-no 1 :stage-no 1}] (fn get [key] (. state key)) (fn update [key value unclamped] (let [[low high] (or (?. limits key) [nil nil]) clamped (if (and (not unclamped) (and low high)) (clamp value low high) value)] (tset state key clamped))) (fn adjust [key value unclamped] (let [[low high] (or (?. limits key) [nil nil]) cur (get key) clamped (if (and (not unclamped) (and low high)) (clamp (+ value cur) low high) (+ value cur))] (update key (+ (get key) value)))) {: get : update : adjust})) (local state (mk-state)) (fn score [] (state.adjust :score (* (state.get :tires) (state.get :speed)))) (fn go-to-service-or-stage [] (set screen (if (> (state.get :svc-time) 0) :service :stage))) (fn refill-svc-time [] ;; after spend and rest this will re-set the state for next time ;; reset svc-time depending on stage-no (state.update :svc-time ;; 1 2 3 4 5 6 7 8 9 10 ... (???????) (. [ 0 45 0 30 0 90 0 45 0 30 0 45 0 0 30 0] (state.get :stage-no))) ;; xxx add some card options, ensure 5 choices )

Pace Note Log

This is a list of messages that are displayed when a card is not selected in the stage view, or when you finish the rally:

fennel source: :noweb-ref gamelog
(var msgs [{:c 11 :s 15 :m "SS1: Start"}]) (fn log [msg c s] (table.insert msgs {:m msg :c (or c 11) :s (or s 15)})) (fn pace-note-str [note] (case note {:dir :ahead :speed accel} (.. "> " accel "0 yards ahead") {:dir dir :speed limit :fn call :badge badge} (.. "> " dir "!! " limit " " badge) {:dir dir :speed limit} (.. "> " dir " " limit))) (fn log-pace-note [note] (log (pace-note-str note) 5 6)) (fn log-final-score [] (log (.. "Final Score: " (state.get :score)) 11 2) (if (> (state.get :score) (pmem 1)) (do (log (.. "Previous(!) High Score: " (pmem 1) " on SS" (pmem 2)) 11 2) (pmem 1 (state.get :score)) (pmem 2 (state.get :stage-no))) (do (log (.. "High Score: " (pmem 1) " on SS" (pmem 2)) 11 7))) (log "Reset the console and try again!" 5 15) (log "Thanks for playing!" 5 15))
fennel source: :noweb-ref interface
(fn rndr-log [idx- x y w rows rev] ; scrolling text region (let [idx- (or idx- idx) yshft (* idx- 8)] (for [i idx- (math.min (+ idx- rows) (length msgs))] (let [{:m msg :c c :s s} (. msgs (if rev (- (+ 1 (length msgs)) i) i)) y2 (+ y (- (+ 2 (* i 8)) yshft))] (sprint msg (+ x 3) y2 c s)))) ; scrollbar )
fennel source: :noweb-ref handlers
(fn tic-log [] (when (btn 0) (set idx (math.max (- idx 1) 1))) (when (btn 1) (set idx (math.min (+ idx 1) (length msgs)))) (when (or (btnp 4) (keyp 50)) (reset)) (cls 0) (rndr-log idx 2 2 254 16 false))

Pace Notes

These functions are applied to notes to create hazards:

fennel source: :noweb-ref note-mods
(fn water-hazard [note card] (when (> (state.get :speed) 3) (state.adjust :speed -2)) (log "Splash!!" 12 8) true) (fn caution-jump [note card] (when (> (state.get :speed) 3) (state.adjust :car (* (math.random 3) 10 -1)) (when (< (math.random 10) 1) (state.adjust :car -10) (log "Ouch! Took suspension DMG!"))) true) (fn big-cut [note card] "ony resolve cut if the cut card is played" (when (= (. card :id) :corner-cut) (if (> 20 (math.random 100)) (do (state.adjust :car (if (> 20 (math.random 100)) -99 -20)) (log "We took heavy dmg that cut!" 2 7) (log "Be careful!" 2 11)) (do (state.adjust :speed 2) (when (> (state.get :speed) (. note :speed)) (state.adjust :car 10))))) true) (fn lock-in [note card] (let [next-note (. stage (+ 1 (state.get :note-no)))] (draw 1) (if (= (. next-note :dir) :ahead) (state.adjust :speed (. next-note :speed)) (state.update :speed (. next-note :speed))) (state.adjust :note-no 1)))

And are used here:

fennel source: :noweb-ref pacenotes
(set stage [ ; SS1 is pre-ordained {:dir :ahead :speed 3} {:dir :left :speed 2} {:dir :ahead :speed 4} {:dir :caution :speed 1 :fn caution-jump :badge :caution-sign} {:dir :right :speed 3} {:dir :ahead :speed 1} {:dir :left :speed 4 :fn big-cut :badge :corner-cut} {:dir :ahead :speed 2} {:dir :right :speed 1} ]) (fn mk-stage [len] "probabilistic stage gen after first" (var prev nil) (fcollect [_ 1 len] (let [speed (math.random 5) directions [:left :left :right :right :caution :ahead ] hazards [[caution-jump :caution-sign] [water-hazard :water-hazard] ;; below no straight [big-cut :corner-cut] [big-cut :corner-cut]] dir (. directions (if (and prev (= (. prev :dir) :ahead )) (math.random 5) (math.random 6))) hazard (. hazards (if (or (= dir :ahead) (< 1 speed)) (math.random 2) (math.random 4))) optional (math.random 25)] {:dir dir :speed speed :fn (or (and (= dir :caution) (?. hazard 1)) (and (<= optional (length hazards)) (?. hazard 1)) nil) :badge (or (and (= dir :caution) (?. hazard 2)) (and (<= optional (length hazards)) (?. hazard 2)) nil)})))

Cards and Deck Setup

fennel source: :noweb-ref cards-and-deck
(local card-defns { :floor-it { :name "Floor it!" :id :floor-it :costs {:focus 1} :effects {:speed 1} :desc "Flat out, lad!!!"} :slow-down { :name "Slow down" :id :slow-down :costs {:focus 1} :effects {:speed -1} :desc (.. "Ease off it, you'll\n" "get us killed!")} :handbrake { :name "Handbrake Slide" :id :handbrake :costs {:focus 2} :effects {:speed -3 :tires -10 :score 250} :desc (.. "not quite the ole\n" "Scandy Flick, eh?")} :corner-cut { :name "Corner Cut" :id :corner-cut :costs {:focus 2} :effects {:score 1000} :desc (.. "20% of time dmg,\n" "20% of that real bad!")} :locked-in { :name "Locked in" :id :locked-in :costs {:focus 2} :effects {:fn lock-in} :desc (.. "One with the car,\n" "One with the gravel.")} }) (fn mk-deck [] (shuffle [ (. card-defns :floor-it) (. card-defns :floor-it) (. card-defns :floor-it) (. card-defns :floor-it) (. card-defns :locked-in) (. card-defns :slow-down) (. card-defns :slow-down) (. card-defns :handbrake)])) (set deck (mk-deck))

Turn Resolution

Here's all the rules for resolving card and stage effects:

fennel source: :noweb-ref resolution :noweb yes
<<resolvers>> (fn resolve [note card] (resolve-provide-resources note card) (resolve-card note card) (resolve-stage-effects note card) (resolve-conditions note card))
fennel source: :noweb-ref resolvers
(fn resolve-provide-resources [note card] "resolve per-turn resources" ; you may later have car mods that help or hinder! (state.adjust :focus 1))
fennel source: :noweb-ref resolvers
(fn resolve-card [note card] (each [k v (pairs (. card :costs))] (if (= k :fn) (v note card) (state.adjust k (- v)))) (each [k v (pairs (. card :effects))] (if (= k :fn) (v note card) (state.adjust k v))))
fennel source: :noweb-ref resolvers
(fn resolve-stage-effects [note card] (case note {:dir :ahead :speed accel} (do ; he he he (if (= (. card :id) :floor-it) (when (< (state.get :speed) accel) (state.update :speed accel))) (state.adjust :note-no 1)) {:dir dir :speed limit :fn call} (do (call note card) (when (> (state.get :speed) limit) (state.adjust :car -10)) (state.adjust :note-no 1)) {:dir dir :speed limit} (do (when (> (state.get :speed) limit) (state.adjust :speed -1) (state.adjust :car -10)) (state.adjust :note-no 1))))
fennel source: :noweb-ref resolvers
(fn resolve-conditions [note card] (when (= (state.get :note-no) (length stage)) (score) ;; theres one here bc zeroing ;; the speed zeroes score too (for [i 1 (+ 1 (length hand))] (discard)) (draw 4) (state.adjust :focus 2) (state.update :speed 0) (state.adjust :stage-no 1) (state.update :note-no 1) (set stage (mk-stage (math.min (+ 1 (length stage)) 16))) (log (.. "SS" (state.get :stage-no) " Complete, " "Score " (state.get :score)) 11 2) (log (.. "SS" (state.get :stage-no) " Start") 11 2) (set trees []) (refill-svc-time) (go-to-service-or-stage)) ;; focus damage... ;; probably want some curve here instead (when (< (state.get :focus) 0) (when (> 20 (math.random 100)) (state.adjust (if (% 2 (math.random 100)) :car :tires) (- (math.random 15))))) ;; score (score) ;; end conditions (when (= (state.get :stage-no) 11) (log "You finished the rally! Congrats!" 6 15) (log-final-score) (set idx (- (length msgs) 16)) (set screen :log)) (when (<= (state.get :car) 0) (log (.. "You've crashed on SS" (state.get :stage-no) "!") 2 14) (log-final-score) (set idx (math.max (- (length msgs) 16) 16)) (set screen :log)))

Stage Scene

fennel source: :noweb-ref interface
(fn rndr-stats [] (let [basal 3] ;; bg (for [r 0 2] (for [c 0 (/ 256 8)] (spr 5 (* 8 c) (* 8 r)))) ;; focus (sprite :focus 3 (+ basal 0)) (sprint (.. "FOC:" (state.get :focus)) 12 (+ basal 2) 11 15) ;; speed (sprite :speed 3 (+ basal 8)) (sprint (.. "SPD:" (state.get :speed)) 12 (+ basal 10) 11 15) ;; health bars (sprite :car-health 43 (+ basal 0)) (sprint (.. "CAR:" (state.get :car) "%") 52 (+ basal 2) 11 15) (sprite :tire-health 43 (+ basal 8)) (sprint (.. "TIRE:" (state.get :tires) "%") 52 (+ basal 10) 11 15) (sprite :stage-end 130 (+ basal 0)) (sprint (.. "SS" (state.get :stage-no) " Score:" (state.get :score)) 142 (+ basal 2) 11 2) (when (not (= 0 (pmem 1))) (sprint (.. "Hi:SS" (pmem 2) " Score:" (pmem 1)) 130 (+ basal 11) 11 2))))
fennel source: :noweb-ref interface
(fn rndr-card-info [card x y w] (case card {:name name :id id :costs costs :effects effects :desc desc} (do (sprint name (+ x 1) (+ 8 y) 11 15) (spr (. sprites id) (+ x 80) (+ 8 y) 10 2) (sprint "Costs:" (+ x 1) (+ 16 y) 11 7) (var offset 0) (each [name val (pairs costs)] (sprite name (+ x 1 offset) (+ 23 y)) (set offset (+ offset 8)) (for [i 1 val] (sprite (if (< val 0) :up-arrow :down-arrow) (+ x 1 offset) (+ 23 y)) (set offset (+ offset 8)))) (sprint "Effects:" (+ x 1) (+ 32 y) 11 7) (when (not (?. effects :fn)) (var offset 0) (each [name val (pairs effects)] (sprite name (+ x 1 offset) (+ 39 y)) (set offset (+ offset 8)) (for [i 1 val] (sprite (if (> val 0) :up-arrow :down-arrow) (+ x 1 offset) (+ 39 y)) (set offset (+ offset 8))))) (sprint desc (+ x 1) (+ 48 y) 11 1)) ;; show log when no card is selected _ (do (rndr-log 1 (+ x 1) (+ 8 y) 126 6 true))))
fennel source: :noweb-ref interface
(fn rndr-hand [] (let [basal 67] ;; bg (for [r 9 (/ 144 8)] (for [c 0 (/ 120 8)] (spr 6 (* 8 c) (* 8 r))) (for [c (/ 120 8) (/ 248 8)] (spr 5 (* 8 c) (* 8 r)))) ;; card list (each [i card (pairs hand)] (sprite (. card :id) 3 (+ basal (* i 8))) (sprint (. card :name) 12 (+ basal (* i 8)) (if (= i idx) 11 12) (if (= i idx) 14 14))) ;; (card) effects area (rndr-card-info (?. hand idx) 128 basal 128) ))
fennel source: :noweb-ref interface
(fn rndr-stage [] ;; bg (for [r 3 8] (for [c 0 (/ 248 8)] (spr 7 (* 8 c) (* 8 r))) (for [c 0 (/ 248 8)] (sprite :road-horiz (* 8 c) 54))) ;; bg trees (for [ti 1 15] (let [[idx xrand yrand flip] (or (?. trees ti) [(math.random 2) (math.random 50) (math.random 10) (math.random 2)])] (bsprite (if (= 1 idx) :tree1 :tree2) (+ (* (- ti 1) 20) xrand) (+ 30 yrand) (if (= 1 flip) 1 0)) (when (not (?. trees ti)) (table.insert trees [idx xrand yrand flip])))) (let [step (/ 256 (length stage))] ;; stage progress (bsprite (if (< 15 (% t 30)) :car1 :car2) (- (* (state.get :note-no) step) (/ step 2)) 45) ;; visible notes (each [i note (ipairs stage)] (let [x (- (* i step) (/ step 2) 3)] (when (< i (+ (state.get :note-no) pview)) (bsprite (?. note :dir) x 30) (case note {:badge badge} (when badge (sprite badge (+ 9 x) 32))) (sprint (if (= (. note :dir) :ahead) (.. (. note :speed) "0") (. note :speed)) (+ 2 x) 40 0 12))))) ;; fg trees (for [ti 16 30] (let [tree (or (?. trees ti) [(math.random 2) (math.random 50) (math.random 10) (math.random 2)])] (bsprite (if (= 1 (. tree 1)) :tree1 :tree2) (+ (* (- ti 16) 40) (. tree 2)) (+ (. tree 3) 50) (if (= 1 (. tree 4)) 1 0)) (when (not (?. trees ti)) (table.insert trees tree)))) ;; print readout (let [idx (state.get :note-no) msg (pace-note-str (. stage idx))] (sprint (.. msg "!!") (+ 40 (* idx 3)) 62 11 0)))

and the TIC handler for it:

fennel source: :noweb-ref handlers
(fn select-card [] (resolve (. stage (math.min (state.get :note-no) (length stage))) (. hand idx)) (discard idx) (draw 1) ; resolve may change this lol!! u ded if so (when (= screen :stage) (set idx 0) (log-pace-note (. stage (state.get :note-no))))) (fn tic-stage [] (when (btnp 0) (set idx (math.max 0 (- idx 1)))) (when (btnp 1) (set idx (math.min (+ idx 1) (length hand)))) (let [(x y l m r sx sy) (mouse)] (if (or l r) (when (and (< 0 x 128) (> y 67) (not debounce)) (let [row (guess-row 58 y (length hand))] (set idx row) (when r (set debounce true) (select-card)))) (set debounce false))) (when (and (or (keyp 50) (btnp 4)) (> idx 0)) (select-card)) (cls 0) (rndr-stats) (rndr-stage) (rndr-hand))

Service Park

Data struct close to the top of the final file:

fennel source: :noweb-ref upgrades
(local upgrade-opts [ {:get-card :corner-cut :cost 25 :avail 1} {:get-card :handbrake :cost 25 :avail 1} {:restore :car :val 10 :cost 10 :avail :inf} {:restore :tires :val 25 :cost 10 :avail 4} {:pass true :avail 1} ]) (fn mk-upgrade [prev] "replace PREV with returned val" (let [ids [:corner-cut :handbrake :locked-in :floor-it] ids (icollect [_ id (ipairs ids )] (when (not (= id (. prev :id))) id)) id (. ids (math.random (length ids)))] {:get-card id :cost 25 :avail 1}))

The upgrades' data structures are managed here, all kind of clumped together...

fennel source: :noweb-ref interface
(fn rndr-restore-desc [type val x y w] (sprint (.. "Restore " type " for " val "%") (+ x 1) (+ y 8) 11 15) (sprint "Fixing your car is the best" (+ x 1) (+ y 16) 11 15) (sprint "way to extend your rally," (+ x 1) (+ y 24) 11 15) (sprint "but dont't forget to improve!" (+ x 1) (+ y 32) 11) 15) (fn upgrade-str [opt] (case opt {:get-card id :cost cost :avail av} (values (.. "Gain " id " Card [" cost ":00]" (if (= av :inf) "" (.. " " av "x"))) #(rndr-card-info (. card-defns id) 22 74 194)) {:restore type :val val :cost cost :avail av} (values (.. "Repair " type " by " val "% [" cost ":00]" (if (= av :inf) "" (.. " " av "x"))) #(rndr-restore-desc type val 22 74 194)) {:pass true} (if (> (state.get :svc-time) 10) (values "Rest up" (lambda [] (sprint "Re-focus & start the next stage" 23 82 11 15) (sprint "with full FOC and an extra draw." 23 90 11 15))) (values "Go to stage" (lambda [] (sprint "You're ready for the next stage." 23 82 11 15) (sprint "I'll see you at timing control!" 23 90 11 15)))) _ (trace (fennel.view opt)))) (fn spend-upgrade [idx] (let [upg (. upgrade-opts idx)] (case upg {:get-card id :cost cost :avail av} (when (and av (>= (state.get :svc-time) cost)) (state.adjust :svc-time (- cost)) (when (not (= av :inf)) (when (> av 1) (tset upg :avail (- av 1))) (when (= av 1) (table.remove upgrade-opts idx) (table.insert upgrade-opts 1 (mk-upgrade upg)))) (let [card (. card-defns id)] (log (.. "You realize you can " (. card :name) ".")) (log (.. "You are filled with confidence.")) (table.insert deck card))) {:restore type :cost cost :val val :avail av} (when (and av (>= (state.get :svc-time) cost)) (state.adjust :svc-time (- cost)) (when (not (= av :inf)) (when (> av 1) (tset upg :avail (- av 1))) (when (= av 1) (table.remove upgrade-opts idx))) (state.adjust type val) (log "Your car is ready for more.")) {:pass true} (do (when (> (state.get :svc-time) 10) (if (> (state.get :focus) 5) (state.adjust :focus 1) (state.update :focus 5)) (log "You feel refreshed!" 11 8) (draw 1)) (state.update :svc-time 0)))))

And this is the scene renderer:

fennel source: :noweb-ref interface
(fn rndr-svc-park [] (rndr-stats) ;; bg (for [r 3 (/ 144 8)] (for [c 0 (/ 256 8)] (spr 5 (* 8 c) (* 8 r)))) (let [rbw (. [1 2 3 4 6 7] (math.floor (+ 1 (/ (% t 120) 20))))] (sprint "Welcome to Service Park!" 50 21 rbw 0)) (sprint (.. "TIME:" (state.get :svc-time) ":00") 90 28 (if (> (state.get :svc-time) 10) 11 2) 0) (rectb 20 35 196 101 11) (line 20 78 214 78 11) ;; offerings ;; xxx need to scroll more than 5 offers (each [i offer (ipairs upgrade-opts)] (let [(desc rndr) (upgrade-str offer)] (sprint desc 23 (+ 30 (* i 8)) (if (= i idx) 11 12) 14) (when (= i idx) (rndr)))))

And this is the TIC handler:

fennel source: :noweb-ref handlers
(fn select-upgrade [] (spend-upgrade idx) (set idx 1) (when (<= (state.get :svc-time) 0) (set screen :stage))) (fn tic-service [] (when (btnp 0) (set idx (math.max (- idx 1) 1))) (when (btnp 1) (set idx (math.min (+ idx 1) (length upgrade-opts)))) (when (and (or (btnp 4) (keyp 50)) (> idx 0)) (select-upgrade)) (let [(x y l m r sx sy) (mouse)] (if (or l r) (when (and (< 20 x 213) (< 35 y 78) (not debounce)) (let [row (guess-row 20 y (length hand))] (set idx row) (when r (set debounce true) (select-upgrade)))) (set debounce false))) (cls 0) (rndr-svc-park))

NEXT Title Graphic

I generated this by hand:

  • took an image of a BMW 2002ti Rally from wikimedia commons

  • took FOREST-16 palette from LOSPEC

  • scaled the image down and palettized it against that FOREST-16 in the GUN Image Manipulator

  • used multicolor to convert that image to LUA code

  • converted that to fennel by hand...

This thing carries its own scene renderer and TIC function, too; in the interest of legibility the generated image data is not included in the web view of this document, consult the underlying source or the cartridge itself to inspect the data.

Loader & Scene

fennel source: :noweb-ref title
(fn tic-title [] (let [(x y l m r sx sy) (mouse)] (when (or l r) (set debounce true) (sync 32 0) (set screen :stage))) (when (or (keyp 50) (btnp 4)) (sync 32 0) (set screen :stage))) (local res 239) (fn tomem [dat ofs len] (var ofs ofs) (var y 0) (for [x 1 (length dat)] (let [c (tonumber (dat:sub x x) 16)] (poke4 (+ y ofs) c) (set y (+ y 1)) (when (> y len) (set y 0) (set ofs (+ ofs 240)))))) (when (= screen :title) (tomem pal 32640 95) (tomem gfx 0 res)) (print "Press (A) or RETURN to start!" 20 130 1) (draw 4) (log-pace-note (. stage (state.get :note-no)))

TIC handler

fennel source: :noweb-ref tic
(fn _G.TIC [] (case screen :title (tic-title) :stage (tic-stage) :service (tic-service) :log (tic-log)) (set t (+ t 1)))

Tangle

fennel source: :tangle ~/.local/share/com.nesbox.tic/TIC-80/group-b.fnl :comments none :noweb yes
;; title: group b ;; author: ry@n.rix.si ;; desc: can you survive the autumn rally classic? ;; site: https://arcology.garden/dumb-ideas/group-b ;; license: hey smell this https://arcology.garden/hey-smell-this#20220116T143655.499306 ;; version: 0.1 ;; script: fennel ;; strict: true ;; save-id: group-b-001-algj ; ================ INIT === <<topvars>> ; ================ HELPERS === <<helpers>> ; ================ DECK ACTIONS === <<deck-actions>> ; ================ STATE === <<state>> ; =============== GAME LOG === <<gamelog>> ; =============== NOTE MODIFIERS === <<note-mods>> ; =============== CARDS and DECKS === <<cards-and-deck>> ; =============== PACE NOTES === <<pacenotes>> ; =============== RESOLUTION <<resolution>> ; ================= SERVICE PARK === <<upgrades>> ; ================ SCENE UI === <<interface>> ; ================= SCENE TIC HANDLERS === <<handlers>> ; ================ TITLE GRAPHIC === ; greetz https://github.com/RiftTeam/multicolor <<title>> ; ================ GAME START === <<tic>> ;; <TILES> ;; 000:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 001:aaaaaaaaaaa11aaaa117711a1b7007b1bb7007bbabb77bbaaaabbaaaaaaaaaaa ;; 002:aad2ddaaadf2fbdadbb2bbbddfbb2bfddbbb2bbddfbbbbfdadfbfbdaaaddddaa ;; 003:aaaaaaaaaaa222aadaa222aaddaa22aa949422aa9999aaaaa00a22aaaaaaaaaa ;; 004:aaaaaaaaaaaa222aa000222a00b0022a0bbb022a00b00aaaa000a22aaaaaaaaa ;; 005:f0f0f0f00f0f0f0ff0f0f0f00f0f0f0ff0f0f0f00f0f0f0ff0f0f0f00f0f0f0f ;; 006:dedededeededededdedededeededededdedededeededededdedededeedededed ;; 007:6667666766666666676667666666666666676667666666666766676666666666 ;; 016:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 017:aaaaaaaaaaaaaaaaeeeeeeeee4eee4eeeee4eee4eeeeeeeeaaaaaaaaaaaaaaaa ;; 018:aa2222aaa220002a20000022200000022202222222202222a220222aaa2222aa ;; 019:aaa55aaaaa222aaaaa2225aaaa5225aaa552255a5555555555522555aaaaaaaa ;; 020:aaaaaaaaaaa66aaaaa6666aaa666666aaaa6aaaaaaa6aaaaaaa6aaaaaaaaaaaa ;; 021:aaaaaaaaaaa12aaaaaa12aaaaaa12aaaa222222aaa2222aaaaa22aaaaaaaaaaa ;; 064:adaaaaaadada00aadda0000aaa00bb00da00bb00dda0000aaada00aaaffffffa ;; 065:aa2222aaa222222a2bb2bbb22b222b2b22b22bb22bb22b22a222222aaa2222aa ;; 066:aaaaaaaaaaaa440aaa22440a22228a0a222aa00a22aa00aaaaaa00aaaaa00aaa ;; 067:aadddaaaadddddaaddddddda22aadddaaa2aadddaaa2adddaaaa2dddaaaaa2dd ;; 068:aa888aaaaa889aaa66888776dd888ddddd988ddd76888676aa898aaaaa888aaa ;; 069:aaaacaaaaaacacaaaaacacaaaaeeeeeaaaeeeeeaaaeeeeeaaaeeeeeaaaaaaaaa ;; 080:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 081:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 082:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 083:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 096:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbaaaabbeeaaabeeeeaaabeeee ;; 097:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabeaaaaaabeaaaaaaebeaaaaaebeaaaaa ;; 098:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbaaaabbeeaaabeeeeaaabeeee83388888 ;; 099:aaaaaaaaaaaaaaaaaaaaaaaabeaaaaaabeaaaaaaebeaaaaaebeaaaaa888888aa ;; 100:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 101:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 102:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 103:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 112:83388888bbbbbbbbbbdddb77bb000b77ab0d0bbbff000fffaaaaaaaaaaaaaaaa ;; 113:888888aabbbbbbdabbdddbdabb000daabb0d0daaff000fffaaaaaaaaaaaaaaaa ;; 114:bbbbbbbbbbdddb77bb000b77ab0d0bbbaa000aaaffffffffaaaaaaaaaaaaaaaa ;; 115:bbbbbbdabbdddbdabb000daabb0d0daaaa000aaaffffffffaaaaaaaaaaaaaaaa ;; 116:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 117:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 118:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 119:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;; 128:aaaaaaaaa9b9b9b8abbbbb88abbbb888abbb8888abb88888ab888888abbbbb88 ;; 129:aaaaaaaa8b9b9b9a88bbbbca888bbbca8888bbca88888bca888888ca88bbbbca ;; 130:aaaaaaaaa9b9b9b9ab22222bab2222bbab222bbbab2122bbab2b122babbbb122 ;; 131:aaaaaaaab9b9b9babbbbbbcabbbbbbcabbbbbbcabbbbbbcabbbbbbcabbbbbbca ;; 132:aaaaaaaaab9b9b9bacbbbbbbacbbbbbbacbbbbbbacbbbbbbacbbbbbbacbbbbbb ;; 133:aaaaaaaa9b9b9b9ab22222babb2222babbb222babb2212bab221b2ba221bbbba ;; 134:aaaaaaaaa9b9b9b9abbbbb22abbbbb22abbbbb22abbbbb22abbbbb22abbbbbb2 ;; 135:aaaaaaaab9b9b9ba2bbbbbca2bbbbbca2bbbbbca2bbbbbca2bbbbbca2bbbbbca ;; 144:abbbbb88abbbbb88abbbbb88abbbbb88abbbbb88abbbbb88abbbbbbbaaaaaaaa ;; 145:88bbbbca88bbbbca88bbbbca88bbbbca88bbbbca88bbbbcabbbbbbcaaaaaaaaa ;; 146:abbbbb12abbbbbb1abbbbbbbabbbbbbbabbbbbbbabbbbbbbabbbbbbbaaaaaaaa ;; 147:2bbbbbca22bbbbca22bbbbca122bbbcab22bbbcab22bbbcabbbbbbcaaaaaaaaa ;; 148:acbbbbb2acbbbb22acbbbb22acbbb221acbbb22bacbbb22bacbbbbbbaaaaaaaa ;; 149:21bbbbba1bbbbbbabbbbbbbabbbbbbbabbbbbbbabbbbbbbabbbbbbbaaaaaaaaa ;; 150:abbbbbb2abbbbbb2abbbbbb2abbbbbbbabbbbbbbabbbbbb2abbbbbb2aaaaaaaa ;; 151:2bbbbbcabbbbbbcabbbbbbcabbbbbbcabbbbbbca2bbbbbca2bbbbbcaaaaaaaaa ;; 160:aaaaaa7aaaaaaa7aaaaaaa77aaaa77e7aaaaa777aaa77777aa7a7e77aaa77a77 ;; 161:aaaaaaaaaaaaaaaa7aaaaaaaaaaaaaaa6aaaaaaa677aaaaa7eaaaaaa77a7aaaa ;; 162:aaaa55aaaaaa5f22aaaaa5ffaaa552afaaa5ffff5552227f55fff577a2f1f77f ;; 163:aa55aaaaa55f5aaa25fa5aaa75ff75aa7af5f2aaaffa2aaaff7aa5aafaaa55fa ;; 176:aaaa7777aa777a77aa77aef7aaaaaa17aaaaaa1faaaaafffaaaaf1ffaaaaaaaa ;; 177:77ee7aaa7a777aaa7faaaaaa77aaaaaaeaaaaaaaffaaaaaaaff1aaaaaaaaaaaa ;; 178:a221ffffaa1faa1faaaaaa1faaaaaaffaaaaaaffaaaaafffaaaaafafaaaaaaaa ;; 179:fa775f2afa772f2afffff2aafaa55aaafaaaaaaafffaaaaaaafaaaaaaaaaaaaa ;; </TILES> ;; <WAVES> ;; 000:00000000ffffffff00000000ffffffff ;; 001:0123456789abcdeffedcba9876543210 ;; 002:0123456789abcdef0123456789abcdef ;; </WAVES> ;; <SFX> ;; 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000 ;; </SFX> ;; <TRACKS> ;; 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 ;; </TRACKS> ;; <PALETTE> ;; 000:0f2c2e692f11913636ad5f52c89660e1c5846b7f5c3456443d708564988e00f600ecddbabda18fa17d5e796e6389542f ;; 001:0f2c2e692f11913636ad5f52c89660e1c5846b7f5c3456443d708564988e00f600ecddbabda18fa17d5e796e6389542f ;; </PALETTE>