I forgot hanzi is two columns wide
Drop flipped dragons when free dropping
Turns out I had false memories about dragons card collection
I've been playing Shenzhen I/O these days and find the minigame entertaining. As an excuse, I'm writing this in Rust, which I've been learning for less than a month.
The rules are explained on Shenzhen I/O Fandom: Shenzhen Solitaire.
As a Rust beginner who transitioned from C++, the architecture is pretty messy. Here's how it works:
main()
calls ui()
ui()
creates mut state: GameState
, a struct defined in
game.rs
ui()
begins an event loop, where for each frame (30 ms[a]):
render
(render.rs
) renders state
with ratatuiGameState::animate
(animate.rs
) mutates
animation-related attributes of state
game.rs GameState::check_if_player_won
)collect.rs GameState::collect
)collect.rs GameState::find_exposed_dragon
)mouse.rs handle_mouse
)
enum UIElement
in
layout.rs
)MouseResult
to event loopmouse.rs GameState::drag
)state
mouse.rs GameState::drop
)state
with a fresh
GameState
[a] Might be less than 30 ms
A major hassle in development is the separation of Slot
s and
UIElement
s. A Slot
holds cards. A UIElement
is just a rectanglular
area on the screen, so given coordinates, we know what the player is
interacting with; and given an element, we know where to render it.
However UIElement
is useful in game logic, so it's used pretty much
everywhere, even when the code is not really UI-related.
What GameState::drag
does is:
state.dragged_cards
state.dragged_from
to a UIElement
What GameState::drop
does is:
state.dragged_from
GameState
does not have access to ratatui or crossterm APIs. Therefore
it cannot directly animate the screen. Instead, it can update internal
state (the flying_cards
and free_dropping_cards
attributes), so the
render
function can calculate the position of each animated card.
We will discuss flying_cards
as an example.
When GameState::collect_{flower, numbered}
find a card to collect, it
calls GameState::fly
with these arguments:
card: Card
from: UIElement
to: UIElement
GameState::fly
will push a new Animation
to its flying_cards: Vec<Animation>
.
Then, for each frame, the event loop will call GameState::animate
, to
advance every animation one frame. When an animation finishes,
GameState::animate
places the animated card in the slot where it
belongs, and removes the animation from the Vec. At this point, this
animation is complete.
free_dropping_cards
is similar. It is played when player wins. The cards
fall below the bottom edge and are discarded instead of landed.
This is an ncurses-like program, and is harder to debug. If the game
panics, the backtrace will be an absolute mess, because it shut down
without leaving ncurses mode. If you need the One True Debugger, my
workaround is to write eprintln!
, redirect it to a file, and reset
the
terminal:
RUST_BACKTRACE=1 cargo run 2> /tmp/bt; reset; cat /tmp/bt
If you supply an argument, the program will treat it as a filepath and
initialize cards in the stack slots from the file. An example file is
sorted.txt
. First row is stack_slots[0]
and so on. It doesn't check
how many cards are there.