While I was exploring traditional deck games I found and loved Jean-Baptiste Vincent’s Card Crawler. Unlike the other card games I’ve worked on to this point, Card Crawler has a map created from a grid of 25 cards. When I play at home it just barely fits on my game table, and I was concerned about ‘tablespace’ on the app as well. The other difference is Aces have a considered value of 1 rather than 14.
The Deck we generate is also a little different. Hearts 1-10 are reserved for HP tracking. The player starts with Spades, Clubs, and Diamonds 1-6:
player_deck = [Card(value,suit) for value in range(1,7) for suit in suits[1:]]
The remaining 25 cards are the rest of the deck and the Jokers.
remaining_values = [Card(value,suit) for value in range(7,11) for suit in suits[1:]] royals = [Card(value,suit) for value in range(11,14) for suit in suits] joker = Card(0,joker_suit)
My grid math was rusty so I took a little time to sort out how to use a single-level array for my grid.
if not n%5, can move left
if n%5 < (5-1), can move right
if n >= 5, can move up
if n < 20, can move down
Now we have a functional grid. The player can only “move” to an orthogonal card, so having them click the gird itself eliminates the need for a DPAD or some other directional control. Our functions will only need to consider the move and not if it’s legal.
I thought about how to handle the grid data and if I should expand the card class to store additional data beyond suit/value. We need to know if a card on the grid has been flipped or not. Additionally, if a monster is not looted before the hero leaves the room it cannot be looted again, but treasure can still be looted after the fact. So that’s two statuses: visible and lootable.
I decided to work off the base class. When we generate cards now we mark the starting deck as visible and unlootable, and do the reverse for the cards in the grid.
class Card(object): def __init__(self, value, suit): self.value = value self.suit = suit class CrawlerCard(Card): def __init__(self, value, suit, visible, loot): Card.__init__(self,value,suit) self.visible = visible self.loot = loot
And what I learned from earlier games is the lifting needs to be done in Python functions, not screen language. So we will just call a single function, Move, and go from there. Voila, we now have fog of war–the map is revealed as we explore.
Next we need to maintain a hand, and then the actions available depend on the room the player is currently in. Since this is a deckbuilder with no round limit (the end condition is 0 hp), we can theoretically deal infinitely, so this game is most like Card Capture in its hand-management mechanics. We automatically fill the hand to the limit each time, and if the deck runs out, we shuffle the discards, add them, and continue dealing.
Now, something to keep in mind with this game is cards in hand are only relevant in terms of value (action points) and if it’s a Diamond, which will be worth a bonus at the end of the game. We will change that later, but for now all we’re really looking at is the value for points to spend.
We add a discard function, and for testing we go ahead and add a way to discard the hand.
Next, the most basic action is looting treasure. We add a basic swap function, we’ll refine the rules later.
# loot mechanic, swap card in hand for current location card def swap(self,card): gained_card = self.dungeon[self.hero_index] gained_card.loot = False self.current_hand.remove(card) self.discard_deck.append(gained_card) self.dungeon[self.hero_index] = card
Now we have a basic card economy set up so we will update the move function to require a discard as the cost.
Now it’s time to address monsters. They have 3 states.
- Alive monsters are Spades or Clubs < 11 or Kings that have not been killed. (loot False)
- Lootable monsters have just been killed. (loot True)
- Once the player leaves the room the Lootable monster is effectively dead or vanished, and the easiest way to deal with that is to simply remove them from the game entirely but at some point I think I’d like to at least leave a gravestone or marker.
So for now, having a loot field is adequate to identify them.
There are some other monsters, though. Kings of any suit are a type of MiniBoss. And sometimes a Spade or Club is not a monster, it’s a companion or event.
- set up working grid-based movement
- implemented basic actions move/loot
- set up rules to define active monsters
- set up hand management
- implement queens/jacks info screens and added Jack of Spades choice option, packed strings into dict
- added trash function
- added tableau function for relevant Queens
- added options menu
- added function to reveal adjacent rooms when events triggered
- TODO: ranged attack not launching, look at active monster check
Still thinking about the most intuitive way to select cards. Sometimes you only select one but some require multiples. May go to Card Capture’s method of select action > select card(s) < Confirm to avoid errors.
I think I will have the map direction still be selectable from the grid, and everything else is a “room action” and you select from the list.
- resolved monster check
- TODO implement ability to select cards for defense at turn end /after attack
After some thought I decided to put this one aside for now. I think the map and vibe add just enough complexity it doens’t quite “fit in” with the other card games, which I am going to combine into a single app for personal use. This is how far I got: