Joegame does the Spring Lisp Game Jam 2026
As we are hard at work on the next big thing, Joegame studios thought it wise to do a little bit of professional development and take part in our first ever "game jam." For those who don't know, a game jam is when actual pieces of fruit are used, as opposed to a game jelly, which just uses juice, and has a smoother, thinner texture.
The jam we are taking part in is the Spring Lisp Game Jam 2026. The forthcoming Solitaire RPG epic from Joegame studios has us working in the static, compiled world of zig, which has been great, but taking a break to come back to our old friend in lisp languages. While there are already plans here at joegame to work with Hoot, which appears to be a kind of sponsor of the jam itself, what is more on our mind at the moment is lua, so we decided to go with fennel and love2d, and specifically this lovely template.
What follows aims to be some pretty extensive rubber ducking of the whole process.
Day 1
Our addition to the template is just to assure our dependencies through a simple nix flake:
{
description = "slotaire";
inputs = {
nixpkgs.url =
"github:NixOS/nixpkgs/fad271db9413e9ea187306aa67b26405a818449d";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
love
(lua.withPackages (l: with l; [ busted love fennel ]))
fennel-ls
fnlfmt
# just in case we are looking at lua libs
emmylua-ls
];
};
});
}
And then per the instructions for fennel-ls, download the love docset and add a flsproject.fnl file:
{:fennel-path "./?.fnl;./?/init.fnl;"
:lua-version "lua5.1"
:libraries {:love2d true}
:extra-globals "love.handlers pp"
}
Because I hate prefix arguments, I set the dir local for the project to default to love when running fennel-repl:
;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") ((fennel-mode . ((fennel-program . "love ."))))
… and we are off to the races!
What's the game?
I have been in deep meditation on solitaire. This comes from a larger project for joegame, which will be announced soon. On perusing the various pieces of literature on the subject (directed by the wonderful resources at Solitaire Labratory), one particularly caught my eye, even though it is not applicable to the aforementioned project. The book Chambers Card Games for One introduces the game "Accordion" like so:
Accordion is the simplest of all patience games, so simple that it probably wasn’t invented consciously but just evolved, or —- more accurately — just happened. It has acquired the name Accordion because it takes place in one line of cards that during the game tends to get longer and shorter, rather like the way an accordion does when it is being played.
Perhaps the biggest reason why I became attracted to the game is specifically because it requires no set up playing by hand. You have a deck, you start playing. In this, it becomes an ideal idle activity: sitting on the couch, I have the deck in my hands, I can just start to do it. There are two possible moves: place a card down from the deck and increasing the size of the stack, or moving an existing card to an appropriate spot. Chambers describes the valid moves of the second type like so: "A card that is played to the right of a card that it matches in either suit or rank can be packed upon it. Similarly, if a card matches in suit or rank a card third to its left, it can be packed upon it (ie the card will have to jump over two other cards)" (3). These are the only two moves, but one move may lead to another. As you place cards with these moves, cards
For our purposes now we will define some terms: a "stack" is a pile of cards, whose valid moves are determined by the top card; the "line" is the line of card stacks we are adding to our manipulating; a "deal" is when a card is drawn and pushed onto the line, creating a new stack of one card; a "move" is moving one stack ontop of the other.
Now, already I would argue that while we are just reading from a book, this is a worthwhile game to try and make video. But there is something that has come organically out of pen and paper iterations of Accordion. In the real world, you only have so much space to play, so as you add cards to the right, increasing the line, you eventually have to continue the line somewhere else. This organically starts to make a grid of cards. Chambers indeed notes this point: "If the line gets so long that there is no space for further cards then a second line is started below the first, but the two lines must be considered as one continuous line" (ibid. emphasis mine).
In this, with a flash of inspiration, our whole pitch for the game becomes this: the solitaire game of Accordion, except played on an explicit grid, where vertical moves are available; plus, time permitting, creating roguelike/balatro elements where there is scoring/combos and power up items like, e.g., "clear a column", "delete a card". We shall call gridthuselah, as another name for Accordion (according to Chambers) is Methuselah.
Figure 1: By Bartolomé Bermejo - Source: Web Gallery of art http://www.kfki.hu/~arthp/html/b/bermejo/index.html, Public Domain, https://commons.wikimedia.org/w/index.php?curid=3170425
Getting something on paper
We will be happy today if we can render a grid of cards, and perhaps have some minimal kind of interactivity. Following the structure of our inherited template, I will make a "game scene". But first, I need something to draw. I will take this lovely asset Elv Games which I have used before, and set up some scaffolding to draw arbritrary cards (note, I have done a lot of this logic before in vanilla lua, so this work was more me transliterating it to fennel).
(local u (require :utils))
(local image-path "elv_Playing_Cards.png")
(fn load-asset []
(love.graphics.newImage image-path))
(local pad {:x 12 :y 15})
(local dimensions {:width 40 :height 66})
(local image-dimensions {:width 832 :height 576})
(fn make-quad-row [row-num]
"row num arg is 0-indexed!"
(icollect [_ col-num (ipairs (u.make-range 0 12))]
(love.graphics.newQuad
;; x
(+
pad.x
(* col-num dimensions.width)
(* col-num pad.x 2))
;; y
(+
pad.y
(* row-num dimensions.height)
(* row-num pad.y 2))
dimensions.width
dimensions.height
image-dimensions.width
image-dimensions.height)))
(fn make-raw-quads []
(icollect [_ row-num (ipairs (u.make-range 0 5))]
(make-quad-row row-num)))
(local all-quads (make-raw-quads))
;; elv asset row order is heart, spade, club, diamond
;; our convention is heart, diamond, club, spade
(local card-quads (u.flatten [(. all-quads 1)
(. all-quads 4)
(. all-quads 3)
(. all-quads 2)]))
(local back-quad (. all-quads 6 1))
{
: load-asset
: card-quads
: back-quad
}
Our convention going forward is this: every card in a 52 card deck has an integer ID derived from a deck in our particular order (that is, not NDO), hearts (A-K), diamonds, clubs, spades. These ids are called cid, and given our convention we can make some helper functions already:
(local card-suits {:hearts 1 :diamonds 2 :clubs 3 :spades 4})
(local card-ranks {:ace 1 :two 2 :three 3 :four 4 :five 5 :six 6 :seven 7 :eight 8 :nine 9 :ten 10 :jack 11 :queen 12 :king 13})
(local card-suits-amount (length (u.tkeys card-suits)))
(local card-ranks-amount (length (u.tkeys card-ranks)))
(fn to-cid [rank suit]
(+ (. card-ranks rank)
(* card-ranks-amount (- (. card-suits suit) 1))))
(fn card-suit [cid]
(. (u.tkeys card-suits)
(+ (if (= (math.fmod cid card-ranks-amount) 0) 0 1)
(math.floor (/ cid card-ranks-amount)))))
(fn card-rank [cid]
(let [raw-mod (math.fmod cid card-ranks-amount)]
(. (u.tkeys card-ranks)
(if (= raw-mod 0) card-ranks-amount raw-mod))))
Oh.. I missed lisp. I try not to be a big complainer about things made by very smart people, always chalking it up to my own inability to understand something, but here, to me, shows a few places where 0-indexing would produce something even cleaner, basically anyplace where math determines an index we then need to look up. We should note also that we have our first of realistically many bits of dreaded premature optimization here: settings things up such that decks might be non-standard by having the helper functions refer to our global enum thingys for suit/rank, even though our asset hard codes a standard 52 card deck.
First bit of a game
Based on our working model so far, we can represent much of our game by a few simple data structure: our line of stacks. There also needs to be some kind of deck, which is essentially a stack not on the line, prepopulated with cards. An early POC could be our grid and ability to move stacks ontop of each other. In this, we need some kind of interface to move among the stacks. Solitaire is usually pretty mouse centric… but for now we are going to use a keyboard driven mechanic, so we will need to keep track of a target-cursor and source-cursor.
(local *game-line* (make-line))
(local *deck* (u.knuth-shuffle (u.make-range 1 52)))
(local *source-cursor* {:x 1 :y 1})
(local *target-cursor* {:x 1 :y 1})
The way I have learned to think about video game software, which like most things for me has just been felt out over the years, is to create a pretty strong seperation between the "game" (state and possible manipulations of state) and everything else (view and ui/interaction). In a way, it's just model-view-controller. But the important thing here is simply the idea that there should be portion of your code that is as decoupled as possible from the framework and precise visual representation of the game, which exposes some kind of interface. Then another part of your program is something that reads the state and shows it, as well as captures input and decides if that will manipulate the state.
So, we have our state. What is our interface for it? Well, we need to be able to deal from the *deck*, we need to be able to set the source and target cursors, we need to be able to merge stacks, pushing cards over on the line in consequence. Probably tonight we can get the first two..
(fn deal [line deck]
(let [next-pos (next-empty-stack-pos line)]
(if next-pos
(tset line
next-pos.y
next-pos.x
(+ 1 (length (. line next-pos.y next-pos.x)))
;; val
(table.remove deck))
false)))
(fn move-cursor-down [cursor ?n]
(set (. cursor :y) (math.min grid-info.height (+ (. cursor :y) (or ?n 1)))))
(fn move-cursor-up [cursor ?n]
(set (. cursor :y) (math.max 1 (- (. cursor :y) (or ?n 1)))))
(fn move-cursor-right [cursor ?n]
(set (. cursor :x) (math.min grid-info.width (+ (. cursor :x) (or ?n 1)))))
(fn move-cursor-left [cursor ?n]
(set (. cursor :x) (math.max 1 (- (. cursor :x) (or ?n 1)))))
Further, on the other end of our mental model, we need some "draw" functions for our state. Or we wouldn't be able to see anything!
(fn draw-grid [line]
(for [y 1 grid-info.height 1]
(for [x 1 grid-info.width]
(let [this-stack (. line y x)]
(if (stack-emptyp this-stack)
(cards.drawVEmpty state (get-grid-pos x y))
(cards.drawV state (top this-stack) (get-grid-pos x y)))))))
(local cursor-pad 5)
(fn draw-cursor [pos ?color]
(let [cpos (get-grid-pos pos.x pos.y)
[r g b] (or ?color [0 0 1])]
(love.graphics.setColor r g b)
(love.graphics.rectangle "fill" (- cpos.x cursor-pad) (- cpos.y cursor-pad)
(+ (* cards.default-scale cards.dimensions.width)
(* cursor-pad 2))
(+ (* cards.default-scale cards.dimensions.height)
(* cursor-pad 2)))))
Thinking it like this, suddenly everything comes together at the very end:
{:activate (fn activate [])
:enter (fn enter [])
:leave (fn leave [])
:draw (fn draw [message]
(draw-cursor *source-cursor*)
(draw-cursor *target-cursor* [0 1 0])
(draw-grid *game-line*))
:update (fn update [dt set-mode])
:keyreleased (fn keyreleased [key _ _]
(if (= key "d")
(deal *game-line* *deck*))
(when (= key "space")
(set *source-cursor*.x *target-cursor*.x)
(set *source-cursor*.y *target-cursor*.y))
(if
(= key "down") (move-cursor-down *target-cursor*)
(= key "up") (move-cursor-up *target-cursor*)
(= key "right") (move-cursor-right *target-cursor*)
(= key "left") (move-cursor-left *target-cursor*)))}
We have our *state*, our various interfaces into it (deal, move-cursor-..), and we draw it. Seems alright for day 1!
Figure 2: The game at the end of day 1. The source cursor is blue and the target one is green
Day 2-3
Busy…
Day 4
Ok following from where we left off, the next step is allowing the user to move stacks around. I already know this is going to immediately launch us into a bulk of the complexity, even given that I am choosing to start with no rules. What we want to get done is this (still kinda abbreviated day): the user can move any stack to any other stack and when spaces are made, cards will fill in from the bottom
;; one move
v--------<
1. A--B--C--D--E--F
2. A--E--C--D-- --F
3 A--E--C--D--F
Visualizing what we want here, I want to notice two things: what is originally the B stack in time 1, becomes a stack with two cards, E ontop of B, and its probably a good idea to track this, rather than just deleting the B; there is a difference between 2 and 3, but not because of user input. It could be the case that we start with 1 and move directly on to 3, where the stack combination and filling in are one move, but I have this intuition that we should consider the filling in as a kind of move. Not only will this make it easier, I think, to eventually animate these card movements, making what happens at each step clear, but it means that the filling in behavior itself needs to be parameterized in a way, a kind of rule to follow that we could eventually decide to to play with. What if.. things only fill within a row for example? Or there was a kind of time freeze mode where a user can make a column move in the hole left from the last move? This is either way an important fork in the road: the time cost here is, er, pretty critical because its like a game jam and we did kinda miss 1/5 of it this weekend (although I thought about it a lot!). But either way, I just have this sense that doing it the harder way will pay off here.
So, mr rubber duck, what is next? We already have our cursors, and I also was thinking about the cursors. Or the whole source/target cursor thing. If we had just a dpad and a single button, what would we do? Lets instead imagine there is just one cursor, and the idea of a selected stack. If the user presses the button, we check if a stack is already selected; if a stack is, and it is not currently where the cursor is, then move the selected stack ontop of the cursor; if there is not a selected stack, select the stack under cursor; if the stack selected and the cursor are the same, deselect the stack. Make sense? does to me, more intuitive than it sounds I think but we will see!
This is what we will get done today.
Day 5
OK feelin the pressure now, its halway and we have next to nothing! To update, all the things discussed yesterday were seen to, left today with at first finishing up mouse controls, using the same "one button" DWIM approach. Does one discuss DWIM for video games?
A note on conventions
;; game actions, closely tied to user input and the ongoing progression of the game are `functions!`:
(fn deal! [line deck]
(let [next-pos (next-empty-stack-pos line)]
(if next-pos
(tset line
next-pos.y
next-pos.x
(+ 1 (length (. line next-pos.y next-pos.x)))
;; val
(table.remove deck))
false)))
(fn combine-stacks! [line source target]
...)
(fn move-cursor-down! [cursor ?n]
...)
;; etc
;; game state itself is formatted with %, `%variable`
(local %game-state {
:line (make-line)
:counters (make-grid {:fill-in-wait 0.0})
:deck (u.knuth-shuffle (u.make-range 1 52))
:source-cursor {:x 0 :y 0}
:target-cursor {:x 1 :y 1}
})
;; global constants are like `*var`
(local *grid-info {:width 5 :height 5 :pad {:x 10 :y 5}})
I am not sure what's more fennel idiomatic. The reason I didn't just go with full on common lisp patterns like *constant*, +variable+ is because fennel allows for dot access and something like *constant*.field looks odd (but seems to be valid fennel).
dwim
Following through with the reflection on controls, we have a game action like so:
(fn select-or-move! [line source target]
(when *settings.debug-print
(print (: "source %d,%d, target %d,%d, deselected %s"
:format source.x source.y target.x target.y (if (cursor-deselected-p source) "true" "false"))))
(if (cursor-deselected-p source)
(do
(set source.x target.x)
(set source.y target.y))
(do
(combine-stacks! line source target)
(deselect-cursor! source))))
That is, if nothing is selected, select (think "set as source cursor"); if source cursor is around, make the move and deselect.
rules
Ok, so we are at a good point, new things can come quickly now. Now we should make only valid moves playable. A move is made by referencing a line object (a 2d array of stacks) source and target cursor positions (positions into the line object). Given this information, we should be able to create something that checks for a moves validity. As discussed, cards are represented by integer ids, given a deck with a certain overall order (in rank and suit). We already have those helper functions made, so as specified in Accordion's rules, the two things that make a move valid is the two cards rank/suit, and their relative positions. Both should be straightforward, but we should treat them as separate rules and then compose:
(fn position-right [{: x : y}]
(if (= x *grid-info.width)
{:x 1 :y (+ 1 y)}
{:x (+ 1 x) : y}))
(fn position-right-n [pos n]
(if (= n 0)
pos
(position-right-n (position-right pos) (- n 1))))
(fn position-left [{: x : y}]
(if (= x 1)
{:x 5 :y (- y 1)}
{:x (- x 1) : y}))
(fn position-left-n [pos n]
(if (= n 0)
pos
(position-left-n (position-left pos) (- n 1))))
;; game rules
(local *rules {
:valid-cards (fn [line source target]
(let [source-card (top (. line source.y source.x))
target-card (top (. line target.y target.x))]
(and
source-card
target-card
(or
(=
(cards.card-rank source-card)
(cards.card-rank target-card))
(=
(cards.card-suit source-card)
(cards.card-suit target-card))))))
:valid-positions (fn [_ source target]
(or
(u.v= target (position-left-n source 1))
(u.v= target (position-left-n source 3))))
})
Ok, we still do not have our one little innovation here, but this completes implementing the original rules of accordion: same suit or rank, either one to the left, or three (adjacent cards, or cards separated by two). Importantly, the grid is both a continuous line to us and a grid, which is why we need our special position-left/right. position-up is even easier:
(fn position-up [{: x : y}]
{: x :y (- y 1)})
(fn position-up-n [pos n]
(if (= n 0)
pos
(position-up-n (position-up pos) (- n 1))))
;; then add to the rule
{
;; ...
:valid-positions (fn [_ source target]
(or
(u.v= target (position-left-n source 1))
(u.v= target (position-left-n source 3))
(u.v= target (position-up-n source 1)))) ;; here
;; ..
}
To round this up we need a win state I guess, or rather, just game end situation. The game ends when there are no more moves left, which means the deck needs to be empty and there are no possible moves left. The last bit seems a little tricky… but I think it will be fine to just brute force it and check for every possible valid move. Assuming we can skip self stack moves, that means (* 24 25) at most. Which is fine.
Day 6-7
Polish
Polish is any effect that creates artificial cues about the physical properties of objects through interaction. In this case, artificial means “not simulated.” When two objects collide and the code tells them that they’re each solid and so should rebound with a certain force in a certain direction, this is not polish. This is part of the simulation, part of the game’s response to input. Instead of “artificial,” the terms “nonessential” or “layered on” could also be used. – Swink, Game Feel: A Game Designer's Guide to Virtual Sensation, Chapter 9
Looking through this book, it's clear the author would not consider Gridthuselah a game which should worry much about "feel," but either way, I am interested in this divide between the simulation and the polish, the essential and the nonessential. What is "essential" in a video game, why would you ever include anything that isn't essential? It is the kind of question that is easy for other kinds of "games." For the author of Game Feel (Steve Swink), games have feel only when they have "real time control" in some kind of "simulated space," so the stuff that is "nonessential" becomes everything that doesn't contribute to that.
Does classic Windows Solitaire/Klondike have game feel? The game purports to be a simulation of the world, your cursor is a hand, picking up and dropping cards onto stacks, time moves in real time, as it tells how long the game was in minutes and seconds at the end… Still, according to the framework of Game Feel, real time control seems only to exist where there is an "ongoing correction cycle" (Chapter 2) of the player/input to the action on the screen. Asteroids has real time control, but Guitar Hero does not: "the whole loop of input and response happens in less than 100 ms, but once it’s done it’s done. There is no continuous flow of input and response, no correction cycle" (Chapter 4). What Guitar Hero does definitely have is polish, it is noted though.
Not to get too hung up on this, Game Feel is quite open about its somewhat narrow definition and system it sets up. The reason I am thinking about it is that this is where we are at with our little game jam game here that we are still unsure we will be able to finish. By shamelessly borrowing some rules from an old solitaire book, we have (purposefully) made everything that isn't polish quite easy for ourselves. But now that we 3/4 of the way through, the continual problem of what to prioritize gains a lot more traction. And here I am reading books.
What are we polishing?
Figure 3: The game as it stands as a blurry gif. While cards 'fill in' in a kind of interactive, 'real time' way, everything else is discrete, cards appear and disappear
I think.. our filling-in intuition was good. It is already barely understandable what is going on when a stack on the line is left empty and cards start to cascade back/to-the-left to fill in, but making it not immediate helps, and also already suggests our next move: cards should not jump from discrete spots, they should move/float to them. I believe we glossed over the actual implementation of this before, better look at it now.
Following a kind of hybrid "structure-of-arrays" situation, we store values for each stack on the line in a separate table, where the :counters table here matches up exactly with the :line one.
(local %game-state {
:line (make-line)
:counters (make-grid {:fill-in-wait 0.0 ;; here
:target-pop 0.0
})
:deck (u.knuth-shuffle (u.make-range 1 52))
:source-cursor {:x 0 :y 0}
:target-cursor {:x 1 :y 1}
:active-rules [:valid-cards :valid-positions]
:game-done false
})
With just this simple idea, we can control any arbritrary thing that might deal with a given stack across time. So, to implement the delay of filling in, we are always checking if something should be filled in, and if a delay is already not in progress, one is set. At the same time, any delay in progress (any stack with :fill-in-wait > 0) is updated by decrementing that value by dt.
{
:update (fn [dt _set-mode]
(iter-grid %game-state.counters
(fn [val]
(let [{: x : y : it} val]
(if
;; fill in wait check
(> it.fill-in-wait 0)
(let [new-val (- it.fill-in-wait dt)]
;; decrement by new-val
(tset %game-state.counters y x :fill-in-wait (math.max 0 new-val))
;; the wait is over
(when (< new-val 0)
(combine-stacks! %game-state.line (position-right val) val)
(check-finish-game! %game-state)))
;; check fill in
(and
(should-fill-in-p %game-state.line val)
(<= it.fill-in-wait 0))
(tset %game-state.counters y x :fill-in-wait (u.bpm-secs 150))))))))
}
The plan of attack here is simple (I think). The same value we are using to simply delay the call to combine-stacks! can become the progress of the card the next stack over moving to this space. This will clearly play out in draw, which right now displays cards only based on the line state.
(fn get-grid-pos [x y]
"Given grid position, return coordinates of that card"
{:x (+ *grid-info.pad.x (* cards.dimensions.width cards.default-scale (- x 1))
(* *grid-info.pad.x x 2))
:y (+ *grid-info.pad.y
(* cards.dimensions.height cards.default-scale (- y 1))
(* *grid-info.pad.y y 2))})
(fn draw-grid [line]
(for [y 1 *grid-info.height 1]
(for [x 1 *grid-info.width]
(let [this-stack (. line y x)]
(if (stack-emptyp this-stack)
(cards.drawVEmpty state (get-grid-pos x y))
(cards.drawV state (top this-stack) (get-grid-pos x y)))))))
draw-grid needs to know about :fill-in-wait, but in fact that value for the stack to the "left" of it. Then its just about interpolating between the two positions using the value of wait/wait-total-duration.
(fn draw-grid [game-state]
(for [y 1 *grid-info.height 1]
(for [x 1 *grid-info.width]
(let [this-stack (. game-state.line y x)
this-pos (get-grid-pos x y)]
(cards.drawVEmpty %state this-pos)
(if (not (stack-emptyp this-stack))
(let [pos-left (position-left-n {: x : y} 1)
neighbor-fill-in-wait (and pos-left (. game-state :counters pos-left.y pos-left.x :fill-in-wait))]
;; for filling in movement
(if (and neighbor-fill-in-wait (> neighbor-fill-in-wait 0))
(cards.drawV %state (top this-stack) (u.v-lerp (get-grid-pos pos-left.x pos-left.y) this-pos (/ neighbor-fill-in-wait *settings.fill-in-duration)))
(cards.drawV %state (top this-stack) (get-grid-pos x y)))))))))
Just like that, it all works:
Figure 4: Basic fill in implemented.
Day 8
springs
We can do better.
Our basic vector2 lerp looks like this (relying on the lume library that came with our love2d+fennel template):
(fn v-lerp [a b amount]
{:x (lume.lerp a.x b.x amount)
:y (lume.lerp a.y b.y amount)})
Now, amount for us is linear right now, it changes at the rate of frame times, but I know (just because I have done stuff like this before), we can get big "polish" gains by messing with the rate of change proportional to its progress. I first learned about this as a frontend web developer working with libraries like react-spring. How does react spring work?
As an aside, I am not a big AI guy, but if I was allowed to in this game jam, I would be probably paying a few cents towards my openrouter api key to ask something about this. Not "give me fennel function which simulates a spring-based tween/lerp?" but more "how can I adjust my lerp to by more 'spring like'?". Still, I respect and very much appreciate the rules for the jam. While my skills and intuitions may not show it, I have been doing stuff like this a while. I know we are lerping, linear interpolating, and I know there are other kinds of interpolation, that is kinda what we want, right?
Figure 5: Other kidns of interpolation
Ok lets look at Bezier.
Figure 6: Cmglee, CC BY-SA 3.0 https://creativecommons.org/licenses/by-sa/3.0, via Wikimedia Commons
Looks good to me, but we need to imagine it temporally, not spatially. Well but that doesnt work, because we don't have another dimension here… Ok lets think in "first principles" here.. What we have is the movement of an object over time. What we are imagining is that the change is relative to the progress, it moves slower at the beginning, and then speeds up at the end. We just want it to be exponential is all, but not adjust the underlying state of time. Duh, this is so much simpler.
;; where the lerp happens (cards.drawV %state (top this-stack) (u.v-lerp (get-grid-pos pos-left.x pos-left.y) this-pos (math.pow (/ neighbor-fill-in-wait *settings.fill-in-duration) 2))) ;; here
Figure 7: Filling in exponentially
Ok, the movement is (imo) notably more "natural," the polish of the simulation has been increased. One thought for me, is that there is the progress of each card filling in, but like the last gif there, we want there to be the overall progress of filling in, the total cascade. Wouldn't it be great for the filling in to speed up near the end of the total cascade? We could keep track of a single cascade, but our code currently doesn't have a sense of a cascade. It's just checking for fill ins. Lets put that to the side.
We are using our crude blue/green cursors to keep track of target and sources. Let us use the same principles we have so far to scale the cards as we target them. Just following intuition here.
Figure 8: A bug with our popping
Now, what is happening here?
(fn [dt _set-mode]
(iter-grid %game-state.counters
(fn [val]
;; ...
;; ...
;; ...
;; check target pop HERE
(if (u.v= %game-state.target-cursor val)
(when (not (= it.target-pop *settings.pop-duration))
(tset %game-state.counters y x :target-pop (math.min *settings.pop-duration (+ it.target-pop dt))))
(when (not (= it.target-pop 0))
(tset %game-state.counters y x :target-pop (math.max 0 (- it.target-pop dt))))))))
If the current card is the current target, increment the value at least to pop-duration…
… … ..
Ok, well, it was just a bug with our mouse logic.
We have polished both the fill-in and the target actions in the game. We need to now have some kind polish/reactivity arount setting a source. How about a little wiggle? This can be done the same way as the the time stuff, where it is just the question of the right function interpreting the underlying counter.
In a weird way, I am more prepared for this than the movement thing. My heuristic: a wiggle is something that starts in one state, undergoes something, and ends up back where it started; this means we will almost definitely use trigonometry. A sin wave starts at 0 and ends at 0 every period. While a wiggle is also going to be a rotation, it wouldn't need to be to make use of a sin wave. All that is important is the nature of the transformation over time. Here is the simplest, doing a wiggle-times oscillations linearly.
(* *settings.wiggle-amount
(math.sin (*
math.pi
2
(/
(. game-state.counters y x :source-wiggle)
*settings.wiggle-duration))))]
Figure 9: Wiggling the source selection, v1
qol
Polish is one thing, and we will be doing more today, but there is another "thing" here I think about: making all the small things not as frustrating. Right now, our "DWIM" logic has a small annoyance. When you click, it first checks if there is a source set, and then checks for validity. If there is no source, one is set. If it is not valid, the source is deselected. But when playing the game, sometimes I just
Day 9-10
Well you could say yesterday I was really in-the-zone and got a lot done…
TODO
update on polish
Figure 10: Simple waver which will dip from 1 to s
rubber ducking multi card movement
What we want is for cards to return to the deck when a stack gets to a certain size, see new mechanics. How I want to do this is like all our other "tweens," where a counter refers to a certain stack, and the progress of that counter to 0, over some predefined duration. For this specific effect, the idea is to have a stream of cards flow one by one to the deck, ideally speeding up/looking organic as they do. My initial thought is that each card moving over is delayed based on its index (always 1..amount-traveling), but then its speed is adjusted to compensate for the delay. Starting out just linearly and playing around in the calculator, I get y=x+dx-d as a suitable function, where d is some delay amount. From there, its easy to use sin((x*pi)/2) in place of x in the original function.
Figure 11: functions for multi card movement
These functions can stay as is and