Towards a polymorphic solitaire engine in C99, raylib with each variant configured by a small Lua script. So-called because it's the fourth iteration of Gilbert Oddstream's Minimal Polymorphic Solitaire engine.
Development is currently stalled because of the difficulty of making decent scalable cards. Gosol (written in Go + ebiten) is addressing this.
The fundamental objects are:
Csol creates a library of the required number of cards (which depends on the number of packs used, and if any cards were filtered out), thereafter any reference to a card really means 'a pointer into the card library'.
Cards are drawn either using a spritesheet or by Unicode card glyphs. Ideal would be drawing SVG cards direct to the screen (or, rather, creating resolution-dependant spritesheets at runtime), but I can't find a suitable SVG library.
Each pile is a stack of card pointers. Piles come in several types: Cell, Discard, Foundation, Reserve, Stock, Tableau and Waste. Much of the functionality for each type is hard-wired, for example, Cells can only contain a single card, you cannot take a card from a Foundation, you cannot move a card to a Reserve or Stock. Things get a little more interesting for Tableau pile types, which are customised for each type of game using in-built or external scripts.
The different pile types are implemented using a the lightweight object-oriented trick of using embedded structs and virtual function pointer tables.
Contains a list of piles
Each different game is implemented in it's own script (some of which are built-in, coded in C, but most of which are in external Lua files).
- Handles all the graphics and sounds
- Moves the cards from pile to pile
- Turns the cards face up/down
- Defines categories of piles (Cell, Discard, Foundation, Reserve, Stock, Tableau) and enforces the rules that apply to them (for example, a Cell can only contain a single card of any type)
- Creates the cards, sorts them and places them into the Stock pile
BuildPiles
StartGame
AfterMove
AddPile
PileAccept
PileType
MoveCard
MoveCards
SwapCards
CardPairs
CardColor
CardOrdinal
CardSuit
CardProne
CardOwner
Get
Len
First
Last
Empty
StockRecycles
Toast
FAN_*
MOVE_*
Cell, Discard, Foundation, Reserve, Tableau, Stock, Waste tables
BAIZE
Flag | Example | Description |
---|---|---|
--width, -w | --width 640 | sets the window width |
--height, --height | -h 1024 | sets the window height |
--variant, -v | -v "Busy Aces" | specifies the variant to load |
--pack | pack=retro | sets the pack of cards to use |
--noload | --noload | stops the loading of saved.txt |
--nosave | --nosave | stops the creation of saved.txt when exiting |
--nolerp | --nolerp | stops the cards animating |
--noflip | --noflip | stops the cards flipping |
The C creates, manages and frees objects that represent piles, cards and arrays of dragged cards. Handles to these objects are passed (as Lua 'light user data') to Lua functions. These functions, in turn, call C-provided functions to get properties of the objects. For example, C might call the Lua function Tableau.IsPileConformant(pile)
, passing a handle to the pile to be checked. The Lua function iterates through the cards in the pile, like this:
for i = 1, Len(pile) do
local card = Get(pile, i)
local ord = CardOrdinal(card)
end
where Len
and Get
and CardOrdinal
are functions provided by C, that return an integer, a handle to a card object and an number in the range 1 .. 13, respectively.
This approach is fast and flexible, but does require that the script writer knows the constants and function calls available to the used.
Each Pile, Card or Tail object passes from C to Lua would be constructed (by C) as a Lua table, so that you could have expressions like:
if firstcard.color ~= secondcard.color then
return "cards must be in alternate colors"
end
A sequence of cards would be a Lua table array of cards, so you could have:
for _,c in ipairs(tail) do
someFunctionWithCard(card)
end
-- or
for i=1, #tail do
someFunctionWithCard(tail[i])
end
At first sight, this seems good, but it falls apart when accessing a card's owner (each card is owned by a pile) because card.owner
would need to be a pile, represented as a Lua table. The solution to this leads to option 3:
The C maintains a complete copy of the baize, piles, and cards as a tree-shaped Lua table.
The Lua scripts are given access to both the root and branches of the table.
This gets ugly when two things happen (1) the Lua script moves a card; it would seem natural to move it in the table, but then csol would have to parse the tree and update the cards positions, and (2) csol would have the overhead of rebuilding the tree after everu user move (including undo).
Also, a tail of cards being dragged would fall outside this tree.
Using object handles may seem a little clunky, but it offers the best balance of simplicity, effiency and flexibility.
First there was an online version version written in vanilla JavaScript, with scalable SVG graphics. There was an html file for each game variant, and these html files were merged from a standard header, a set of game rules and a standard footer. The variant rules were stored in json tables. Being Javascript, the code grew from something quite well structured, into a seething mess.
Second, there was an Android version written in Lua and using the Solar2D retained mode engine. It gets good reviews and usage in the Google Play Store.
Third, there was a version written in Go, using the immediate mode Ebiten graphics/game engine. The intention was that this version would replace the previous two versions, and provide Linux, Windows, Android and browser-based versions from the same code base. If I had a Mac there would have been iOS and Mac versions, too. But I got discouraged by Go.
This (fourth) version is different because it's much simpler, is written in good old dependable C99, and uses Lua scripts to configure each variant, rather than relying on static tables embedded in the executable. In theory, a player can create and define their own variant, or tweak an existing one, just by editing the Lua scripts.
It may eventually know how to play:
- Aces and Kings
- Acme
- Algerian
- Alhambra
- American Toad (also The Toad)
- Australian
- Baker's Dozen (also Baker's Dozen Relaxed)
- Bisley
- Bristol (also Dover)
- Canfield (also Acme, Storehouse)
- Cruel, Ripple Fan
- Duchess
- Gay Gordons
- Quick Win (an easy to win game, for debugging)
- Fortune's Favor
- Forty Thieves (also Busy Aces, Fortune's Favor, Forty and Eight, Josephine, Maria, Limited, Lucas, Red and Black)
- Freecell (also Eight Off, Freecell Easy)
- Klondike (also Draw One, Draw Three, Batsford, Double Klondike, Gargantua, Thumb and Pouch, Thoughtful)
- La Belle Lucie (and Trefoil, The Fan)
- Mistress and Mrs Mop
- Penguin
- Scorpion, Wasp
- Simple Simon
- Spider (also Beetle, Spiderette, Spider One Suit, Spider Two Suits, Will o' the Wisp)
- Yukon (also Yukon Relaxed)
Some variants have been tried and discarded as being a bit silly:
- Agnes Sorel
- Giant
- King Albert
- Raglan
(I don't see the point of games that you almost certainly can't win; I like ones that have a 33-66% chance of winnning.)
Some will never make it here because they are just poor games:
- Accordian
- Pyramid (or any card matching variant)
- Unlimited undo, without penalty. Also, you can restart a deal without penalty.
- Bookmarking positions (really good for games like Freecell, Penguin or Simple Simon)
- Scalable (TODO), fixed-size and retro card designs
Movable card highlighting (the more useful a move looks, the more the card gets highlighted)This has not been included because the author feels this ruins the essence of playing solitaire.- Statistics (including percent complete and streaks - streaks are great) (TODO)
- Cards spin and flutter when you complete a game, so you feel rewarded and happy (TODO)
- Turn-offable one tap interface. Partial. In order to keep the essence of solitaire play, single tapping on a tableau/cell/waste card will try and send that card to a foundation, nothing more.
- Slightly randomized sounds (TODO)
- Automatic saving and reloading of the game in progress
- Pile scrunching, and a draggable baize; if cards spill out of view to the bottom or right of the screen, just drag the baize to move them into view
A lot a features have been tried and discarded, in order to keep the game (and player) focused. Weniger aber besser, as Dieter Rams taught us. Design is all about saying "no", as Steve Jobs preached. Just because a feature can be implemented, does not mean it should be. Movable card highlighting is one notable feature that fell under this bus.
This solitaire is all about Flow. Anything that distracts from your interaction with the flow of the game has been either been tried and removed or not included. There are no adverts, complicated hard-to-scan cards, distracting background, unnecessary information, or timers.
"A gimmick is a novel device or idea designed primarily to attract attention or increase appeal, often with little intrinsic value."
Also, I'm trying to make games authentic, by taking the rules from reputable sources and implementing them exactly.
Anything that distracts from your interaction with the flow of the game, or the ability to scan a deck of cards, has either been tried and removed, or not included.
This includes: fancy card designs (front and back), changing the screen/baize background, keeping an arbitrary score, distracting graphics on the screen.
The user interface tries to stick to the Material Design guidelines, and so is minimal and tactile. I looked at a lot of the other solitaire websites out there, and think how distracting some of them are. Features seem to have been added because the developers thought they were cool; they never seem to have stopped to consider that just because they could implement a feature, that they should.
Either resize your browser/desktop window (if using scalable cards) or change the settings to fixed size cards. (TODO)
There's no ISO or ANSI or FIDE-like governing body for solitaire; so there's no standard set of rules. Other implementations vary in how they interpret each variant. For example, some variants of American Toad build the tableau down by suit, some by alternate color. So, rather than just making this stuff up, I've tried to find a well researched set of rules for each variant and stick to them, leaning heavily on Jan Wolter (RIP, and thanks for all the fish), David Parlett and Thomas Warfield. Where possible, I've implemented the games from the book "The Complete Book of Solitaire and Patience Games" by Albert Morehead and Geoffrey Mott-Smith.
Backspace
- undoN
- new deal (resigns current game, if started)R
- restart dealF
- find a new gameS
- save current position ('bookmark')L
- load/return to a previously saved positionC
- collect cards to the foundations0
- retro cards1
- small sized cards2
- medium sized cards3
- large size cards4
- four-color cards drawn with Unicode glyphs (clubs: black, diamonds: firebrick red, hearts: darkorchid, spades: darkgreen)5
- two-color Unicode cardsEsc
- exit (current game and position are saved)
Nope, the software doesn't keep an arbitary score. Too confusing. Just the number of moves made, number of wins, and your winning streak (streaks are great). A game isn't counted until you move a card. Thereafter, if you ask for a new deal, that counts as a loss.
You can cheat the score system by restarting a deal and then asking for a new deal, but that's on you.
You can when playing with actual cards, too. Cheat if you like; I'm not your mother.
Nope, there isn't one of those. Too stressful. Solitaire is also called patience; it's hard to feel patient when you're pressured by a clock.
No. The cards are shuffled randomly using a Fisher-Yates shuffle driven by a Park-Miller pseudo random number generator, which is in itself seeded by a random number. This mechanism was tested and analysed to make sure it produced an even distribution of shuffled cards.
There are 80658175170943878571660636856403766975289505440883277824000000000000 possible deals of a pack of 52 playing cards; you're never going to play the same game twice, nor indeed play the same game that anyone else ever has, or ever will.
- For games that start with face down cards (like Klondike or Yukon) the priority is to get the face down cards turned over.
- For games that start with a block of cards in the tableaux and only allow single cards to be moved (like Forty Thieves or Simple Simon), the priority is usually to open up some space (create empty tableaux piles) to allow you to juggle cards around.
- For Forty Thieves-style games, the other priority is to minimize the number of cards in the waste pile.
- For puzzle-type games (like Freecell, Simple Simon and Mistress Mop), take your time and think ahead.
- For games with reshuffles (like Cruel and Perseverance) you need to anticipate the effects of the reshuffle.
- Use undo and bookmark. Undo isn't cheating; it's improvising, adapting and overcoming.
Original games by Jan Wolter, David Parlett, Paul Alfille, Art Cabral, Albert Morehead, Geoffrey Mott-Smith, Zach Gage and Thomas Warfield.
Retro card back designs by Leslie Kooy, jazz cup pattern by Gina Ekiss.
Sounds and card faces by kenney.nl and Victor Vashenko