This is a simple library to give you a jump start on writing your own card games!
In this repo, there are two pieces:
The engine is really a collection of components. You are not required to use all the components (particularly the GameRunner) if you do not want to.
The most fundamental building block of a card game is the Deck. This is an abstract class with the most basic functionality. What is actually used for game play is the PlayingDeck. This is what players will draw from and possibly discard to (there is also a DiscardPile if that is necessary). The PlayingDeck
starts empty and must have other decks added to it. For example:
- Draw poker uses one french deck.
- Casino Blackjack uses multiple french decks.
- Euchre uses one stripped deck
A 52 card StandardDeck is included in the library, but you are free to make your own as well.
Like the Deck
, the Hand is an abstract class with just the base functionality. Two flavors of hands are also included in the library: the FaceUpHand and the FaceDownStack.
Yet another abstract class, the PlayerBase is slightly different because it is generic so that the correct implementation of Hand
can be referenced.
A phase is really just a class that implements the IGamePhase interface. It doesn't have any properties or methods on it; it just signals where you are in game play. If you like, you can create a sealed class that all your phases can then inherit from (gaining all the magic powers that Kotlin bestows on them) but it's not necessary either. Here are the phases in the example project:
sealed class GamePhase : IGamePhase
class Start : GamePhase(), IGameStart
class ReadyToFlip : GamePhase(), IRoundStart
class BattleStarted : GamePhase()
class RoundOver : GamePhase(), IRoundEnd
class GameOver : GamePhase(), IGameEnd
sealed class BattleOutcome : GamePhase()
class BattleWon : BattleOutcome()
class Tie : BattleOutcome()
Note: If using the
GameRunner
, phases must be a class, not an object. They are created through reflection, which does not work with objects.
Your phases can also be marked to distinguish between the start of a game, start of a round, etc. This is there for your use and is currently not used by the engine (but it may be in the future).
Actions implement the IGameAction interface and are how you move from one phase to the next. Here, again, is how they are setup in the example project:
sealed class GameAction : IGameAction
object Deal : GameAction()
object Flip : GameAction()
object CompareCards : GameAction()
object WinnerGetsCards : GameAction()
object AnteUp : GameAction()
object CountStacks : GameAction()
The GameContextBase is an abstract class that will help you bring your various components along from phase to phase. It is the real state of your game. The one property that is not from the library is the cardComparator
. Since every game has a distinct way of comparing the value of cards (i.e. are aces high or low, do suits matter, etc.), you must build this yourself. Here is the cardComparator
from the example game:
class CardComparator : Comparator<Card> {
override fun compare(card1: Card, card2: Card): Int {
return when {
card1.value > card2.value -> 1
card1.value < card2.value -> -1
else -> 0
}
}
}
Finally, the GameRunner. This is your state machine. Use the createRunner(...) method to build it, along with all of your phases. It is generic so the full power of your GameContextBase implementation can be utilized.
Each phase that is going to be used in your game must be registered upfront. You can register the phase using the phase<IGamePhase> method. Every registered phase can have three parts: onEntry functions, onExit functions, and on<IGameAction> transitions (and it can have multiple of each).
Here is a simple example from the included game:
phase<Start> {
onEntry { TermUi.echo("Let's play!") }
on<Deal> {
onDeal()
transitionTo<ReadyToFlip>()
}
}
In the on<IGameAction> lambda, the parameter is the GameContext
on the runner (in the example above, onDeal
is a method on GameContext
). The lambda must also return a KClass, which the helper function transitionTo<IGamePhase>
will do for you.
Once all your phases are registered, all you need to do it call perform(IGameAction)
on the GameRunner
and, if an action has being added for the current phase, the runner will fire every transition lambda and marshal you into the next phase.
And that's it! More is planned for the library, so please stay tuned!