Video game port of the Exploding Kittens card game. Made in Godot as a way to gain experience with the game engine.
Exploding Kittens is a kitty-powered version of Russian Roulette. Players take turns drawing cards until someone draws an exploding kitten and loses the game. The deck is made up of cards that let you avoid exploding by peeking at cards before you draw, forcing your opponent to draw multiple cards, or shuffling the deck.
The game gets more and more intense with each card you draw because fewer cards left in the deck means a greater chance of drawing the kitten and exploding in a fiery ball of feline hyperbole.
- Better understanding of Scene and Node hierarchy.
- Better understanding of the purpose of Resources in a Godot project.
- Understand how to program a turn based card game.
- Dabble in Godot's multiplayer functions.
For more detailed information take a look at the Exploding Kitten fan website.
This section provides a simplified view of the current implementation of the project. It should help in identifying the project parts that could use an improvement as well as make it easier to add features to the existing project.
---
title: Object Relations
---
classDiagram
class Deck{
+int deck_size
+draw_card() Card
}
class Card{
+String card_name
+String card_sub_title
+String card_text
+Texture2D card_texture
+play_card()
}
class SkipCard{
}
Deck "1" *-- "*" Card
Card <|-- SkipCard
Card <|-- CatCard
This section provides an overview on interesting or non-obvious strategies used to implement the game.
Taking into account the class diagram, several scripts were built for the card generalization, namely card.gd and one for each type of card, such as skip_card.gd, obviously skip_card.gd inherits from card.gd. This works because all cards of the same type behave equally and all types of card share a decent amount of attributes. Furthermore, a common scene Card.tscn was defined to represent the card visually, essentially all cards use the same scene, what changes is the script attached to it, that ensures each type of card behaves differently.
The main problem was: There are several cards, that functionally work the same, but have different images and names. That is where Godot resources came in, as per the documentation: Resources are data containers. They don't do anything on their own: instead, nodes use the data contained in resources.. Therefore, resources seemed to fit our use case, small files that are nothing more than data containers for things like card name and path to the card image asset. This set up, allows for easy implementation of several types of cards as well as for easy implementation of several different but same typed cards.
With the components at hand, one could simply load all the resources and dynamically create the cards that would then be statically added to the deck. But what would happen if in the future I want to provide players with the option of picking the cards they want in the deck? Or what if I wanted to add a hundred card deck? Would I manually add each card scene one by one to the deck scene? No... that would be a huge pain in the ass.
Since the Deck Scene has access to the Card Scene, I could programmatically instantiate the card scene and set the corresponding script and the corresponding CardResource. If I can automatize this, then I can create deck lists: simple representations of a set of cards which would make the creation of decks very easy and solve the previously mentioned problems.
The card scene is always the same, and the CardResource allows me to get the corresponding card script (CardResource has the card type which is a one-to-one relation with card scripts). All that is left is choosing the representation for the deck list. I thought of 2 main options:
- Using an int to represent an id
- Using a string that is the path to the card resource
Although using the path works with almost no trouble at all it might represent a future burden and is very error prone, despite being verbose. Using an id seems like the cleaner and more straight forward answer, but it brings with it some implementation choices, namely where to store this int id.
If I store it inside the resource, then that means I have to load every card resource in order to build the deck, even if they aren't needed. For our base game that doesn't seem like a problem, most likely every card is used. However, if the game as a large card pool, in the thousands for example and the game itself only uses a fraction of it, then this approach seemed very wasteful regarding time and space.
The chosen approach was to use the singleton pattern and have a global dictionary that relates id's to resources. This ensures very fast lookup and centralizes the card management, something that proved very useful to provide a method capable of taking an id and returning a Card node. The main problem of this approach is the reverse lookup: What if I have a card node and want to get it's id? The operation in the dictionary is of complexity O(n). Once again, this probably doesn't affect game performance all that much, but adding a card catalogue so that player's can build a deck_list (represented via id's) doesn't seem all that far fetched.
Given a Card, getting it's id can be done in O(1) if the CardResource provides this information. However, that was not the chosen approach, because that would mean that id's are defined in two places, which is not good for consistency, is very much error prone and hinders the process of changing id's or adding cards.
The approach that was chosen was a middle ground, not quite O(1), but way better than O(n) on each lookup. Once the card_factory.gd singleton with the card database is loaded... it creates and stores the reversed dictionary, exchanging memory for speed. Yes, this process still takes O(n) but is performed once, meaning afterwards lookups are still O(1), all while keeping consistency and the id attribution in a single place.
Note: During research the possibility of using a SQL showed up, that might solve this issue in a more elegant manner, but it would be a development burden and simply "overkill" for this project.
As per the Godot docs on node tree structure the more common setup is:
- Node "Main" (main.gd)
- Node2D "Board" (game_controller.gd)
- Control "GUI" (gui.gd)
This means that the main code entry point is clear (inside main), it also allows for the easy creation of game configuration parameters that are defined by the user in Main that then instantiates Board according to them. For example, the cards in the deck and the number of players. Furthermore, GUI will handle all the menus, such as the starting menu or the pause menu. Since my board is static it might as well be a CanvasLayer instead of a Node2D.
With this said, the game loop is defined in Board. With the following simplified code:
class_name GameController extends Node2D
# Member values
enum GameState {PLAYING, DRAWING, FINISHED}
var current_state: GameState
var player_order: Array[Player]
var current_player: int = 0
func _ready():
# Used to start the first player's turn
start_player_turn()
func start_player_turn():
# Advance state to playing cards phase
current_state = GameState.PLAYING
var player: Player = get_current_player()
# Check for victory conditions
if check_victory():
# Winner is found, move game to end state
current_state = GameState.FINISHED
return
# Display current player's hand
player.draw_hand()
func _on_deck_clicked():():
# Cards can only be drawn if someone is playing
if current_state != GameState.PLAYING:
return
# Advance state to drawing phase
current_state = GameState.DRAWING
# Draw Card
var player: Player = get_current_player()
var drawn_card: Card = $Deck.draw_card()
drawn_card.resolve_card(self)
# Go to next player's turn
next_turn()
func trigger_explosion():
var player: Player = get_current_player()
# For the time being, getting an exploding kitten
# eliminates you immediately
eliminate_player(player)Simply put, the code's entry point is the start of the first player's turn. Afterwards, the player might play some cards, but eventually it will have to click on the Deck to draw a card, at which point the _on_deck_clicked function will advance to the turn's next phase (drawing a card from the deck). This card might either go to the players hand or be revealed to the whole table depending on it's type, therefore the method resolve of the card is called. The Base card class adds the card to the player's hand, this method is then overwritten by the Exploding Kitten Card that in this simple example calls the trigger_explosion() which removes the player that drew it from play. At the end of each players turn, but before their card hand is show, victory conditions are checked (in this case, if the player is the only one left in play), at which point the game ends.
The base exploding kittens card game has essentially two phases per turn, the phase where the current player plays as many cards from their hand as they desire and the phase where they draw a card from the deck. These can be easily coded and represented with a simple state machine. In Godot there are two main ways to make state machines:
- Using enums: it's simple and needs minimal code, however it is not as visual as the second approach and the states are not reusable.
- Using Nodes: it's very visual, helps with code modularity and makes the state reusable. However it makes the code's flow harder to follow, leading to longer development times
For this game, where states won't be reused anywhere and the number of states is minimal. The enum approach seemed to best fit our use case, of course if the need arises to refactor the code to the second approach it will be done, but for the time being this solution works well and avoids "overengineering".
---
title: State diagram of the game loop
---
stateDiagram-v2
direction LR
Playing
Drawing
Finished
[*] --> Playing
Playing --> Playing
Playing --> Drawing
Drawing --> Playing
Playing --> Finished
Solution: Use the callback pattern. #TODO: Complete this section


