Skip to content

Deck builder Game


Deck builder core

Published: Sun Jul 06 2025 20:40:46 GMT+0000 (Coordinated Universal Time)

Introduction:

For this tutorial we will be setting up the core mechanics of a simple deck building game that can be expanded on. It will focus heavily on building the game via code.

Instructions will assume you have imported the template project and are using the GMAP hotkeys.

Game play

The player will start with a deck of low-mid value cards

The enemy will start with a deck of mid-high value cards

Four cards from each deck will be dealt, enemy cards on top, player cards on bottom.

The player can pick one or two of their own cards, then target card belonging to the enemy.

If the value of the picked cards are higher than the targeted cards the player can send them all to the discard pile.

If the selected cards are lower value than the targeted card, the player can choose to sacrifice two of their cards to destroy the enemy card. In that case, all three cards are removed from the game. This is the way to remove low value cards from the deck.

The boss cards, cards above 10, can not be sacrificed against. The player has to defeat/purchase them. We will do some deck stacking to make sure they don't show up early and make the game unwinnable.

Once the deck is empty, the discard pile will be shuffled into the player deck.

If the player has too few cards in their deck due to discards, 0 value cards will be shuffled in.

Play continues until the player defeats all the boss cards or is unable to.

Player cards

Description

The first thing we are going to create are buttons with a text attribute that function as the cards of our hand. The buttons will hold the card value, and if they are selected or not. When they are used, their value will be cleared and new value handed to them.

Most everything will be handled via code in the game room, but we still are going to create the player cards as Button scenes with a pressed signal.

Instructions

Create a new scene. CTRL+N, CTRL+A, then select: Button
Set focus to Button in scene tab (CTRL+F8).
Press enter on Button and rename it to PlayerCard.
Attach a script to PlayerCard(CTRL+Equal Sign)

Code Snippet 1

extends Button
#The player will always have a hand of 4, this button will represent one of the four cards given to the player
var value = 0 #This will be overwritten, but 0 will show if card was not dealt properly. If you use 0, give this a different value!
var selected = 0 #If this card is picked

#We're going to find the actual game room and store it here so we can talk to the room.
var game 

func _ready():
	game = get_parent().get_parent().get_parent()
	#making it so the cards are always one size. Makes it look better visually, and prevents
	custom_minimum_size = Vector2(128,128) 
	
func _process(_delta: float) -> void:
	if selected == 0:
		#If the card is not picked, we will have the value of the card, then the word owned.
		text = str(value) + " Owned"
	else:
		#If the card is picked, we will have the value of the card, then the word "selected"
		text = str(value) + " Selected"
#If you haven't, go to node tab and selected "pressed" then press "enter" to connect it here.
func _on_pressed() -> void:
	#if this card isn't selected
	if selected == 0:
		#The game will keep track of what cards we have picked, we only will add this to that list if less than 2 are currently picked.
		if game.selected.size() < 2:
			selected = 1
			#The game will have a function to add to its own array, so we just need to pass "self" which is the instance we have picked.
			game.add_selected(self)
	else:
		#Tell the game to handle removing from array.
		game.remove_selected(self)
		#No longer selected
		selected = 0

Attach signal

Ctrl+F8 to go to node tab

Tab to "Pressed()" and press enter twice to connect the signal to the on_pressed() function.

Press ctrl S and save as default to finish the player buttons.

Boss Cards

Description

We will do a similar task for the boss cards. Do not worry about the functions in the Game yet, those are functions that will be created soon. Similar process of unselecting and passing itself to the Game scene for processing.

Instructions

Create a new scene. CTRL+N, CTRL+A, then select: Button
Set focus to Button in scene tab (CTRL+F8).
Press enter on Button and rename it to BossCard.
Attach a script to BossCard(CTRL+Equal Sign)

Code Snippet 2

extends Button

#Value of the boss card
var value = 0
#If this is targetted or not, only one boss card can be targetted
var targetted = 0
#Going to store game room here
var game

func _ready():
	game = get_parent().get_parent().get_parent()
	#Forcing size to be consistent to help visually and with focus movement.
	custom_minimum_size = Vector2(128,128)

func _process(_delta: float) -> void:
	#If the boss deck is almost out the player has almost won, so print "emtpy"
	if value == null:
		text = " Empty"
	#If not targetted, have value then enemy print
	if targetted == 0:
		text = str(value) + " Enemy"
	#If targetted, print value then target. As player cards are now attacking this card.
	else:
		text = str(value) + " Target"

#If you haven't, go to node tab, and attached pressed() to here while BossCard is picked in scene.
func _on_pressed() -> void:
	#If not targetted, make target
	if targetted == 0:
		#Add target will automatically remove previous target, so game will take care of it.
		game.add_target(self)
		targetted = 1
	else:
		#Set card to not be a target
		targetted = 0
		game.remove_target()

Attach signal

Ctrl+F8 to go to node tab

Tab to "Pressed()" and press enter twice to connect the signal to the on_pressed() function.

Press ctrl S and save as default to finish the Boss buttons.

Game Room

Description

The final scene is our game room. Most of the complex tasks will be in the code, so read over the comments carefully to learn how that works. But we do need to setup our menu correctly in the Godot UI first.

To do this we are going to have a UI object be the game room. Inside it we will a VBoxContainer forming an up and down list like this:

Boss Cards in horizontal container

Player Cards in horizontal container

Fight button

By doing it this way, pressing up and down via keyboard will move you up and down the list, but left/right will let you explore the cards to pick them.

But this will be pretty easy, as most of the work will be done via code.

Instructions

Create a new scene. CTRL+N, CTRL+A, then select: Control
Set focus to Control in scene tab (CTRL+F8).
Press enter on Control and rename it to Game.
Go to inspector, ctrl+f6 to get to inspector tab, ctrl+U to inspector categories.
Property anchors_preset set to 8
Add node VBoxContainer as child of Game(Control Node) CTRL+A to open node create menu.
Press enter on VBoxContainer and rename it to Table.
Add node HBoxContainer as child of Table(VBoxContainer Node) CTRL+A to open node create menu.
Press enter on HBoxContainer and rename it to BossHand.
Set focus to Table in scene tab (CTRL+F8).
Add node HBoxContainer as child of Table(VBoxContainer Node) CTRL+A to open node create menu.
Press enter on HBoxContainer and rename it to PlayerHand.
Set focus to Table in scene tab (CTRL+F8).
Add node Button as child of Table(VBoxContainer Node) CTRL+A to open node create menu.

Press enter on Button and rename it to Fight.


Set focus to Game in scene tab (CTRL+F8).
Attach a script to Game(CTRL+Equal Sign)

Code Snippet 3

extends Control
#Loading the scenes for the cards.
const BOSS_CARD = preload("res://boss_card.tscn")
const PLAYER_CARD = preload("res://player_card.tscn")

#storing in variables for easy reference.
@onready var table: VBoxContainer = $Table
@onready var boss_hand: HBoxContainer = $Table/BossHand
@onready var player_hand: HBoxContainer = $Table/PlayerHand
@onready var fight: Button = $Table/Fight
#We will use these in place of 0,1,2,3,4, etc. later on so it's easier to read. But they all just mean a consistent number.
enum {BATTLE, SACRIFICE,DEFEAT,MORE}

#Will be used to say how many cards per side to be dealt
var cards_per_side = 4
#This array will be what cards are in the player's deck. Cards will be added or removed as game goes on.
var player_deck = [1,1,1,2,2,2,3,3,4]
#When the player plays a card or "buys" a card, they go to the discard. Once the player_deck is empty, the discard gets shuffled into player deck.
var discard_pile = []
#Array holding the boss deck, we want to make sure cards the player can deal with show up first, so we'll stack the deck a little.
var boss_deck = []
#These values will be shuffled and displayed first.
var boss_early_deck = [3,4,4,5,5,6,6,7,8]
#These values will not show up until the early deck values have all been played.
var boss_late_deck = [5,6,7,7,8,9,9,9,10,11,12,13]

#The player can have 2 cards picked at most whose value needs to equal or be greater than target boss card.
var max_selected = 2
#What values are currently picked
var selected = []
#Which of the boss's cards we are attempting to buy/defeat
var target = null

func _ready():
	#Use built in "shuffle" on the array for the three decks
	player_deck.shuffle()
	boss_early_deck.shuffle()
	boss_late_deck.shuffle()
	#Combine the early and late boss deck so won't get any late boss deck cards until early is played out.
	boss_deck = boss_early_deck + boss_late_deck
	
	#Dealing the starting hand for boss
	for i in range(cards_per_side):
		#Creating a boss card instance
		var b_card = BOSS_CARD.instantiate()
		#Removing value from boss deck and giving it to this card
		b_card.value = boss_deck.pop_front()
		#Putting the card in the hboxcontainer that is the cards of the boss
		boss_hand.add_child(b_card)
		
	#Dealing starting hand for player
	for i in range(cards_per_side):
		#Making the button
		var p_card = PLAYER_CARD.instantiate()
		#removing a value from the player_deck and giving it to the dealt card.
		p_card.value = player_deck.pop_front()
		#Adding it as a child.
		player_hand.add_child(p_card)
	#Now that hands are dealt, we set focus to a player card as game starts.
	player_hand.get_child(0).grab_focus()
	
#Function called when a player card needs to be added.
func add_selected(card):
	#If there is room, add the instance to the list of selected player cards
	if selected.size() < max_selected:
		selected.append(card)

#Function called when a card needs to not be selected
func remove_selected(card):
	#Find which card is being removed in array.
	var removing = selected.find(card)
	#setting card to not be selected
	card.selected = 0
	#removing from array
	selected.remove_at(removing)

#Called when pressing to target a boss card.
func add_target(temp_target):
	#If something else is targetted, untarget it.
	if target!= null:
		target.targetted = 0
	#Set target to be the boss card that was passed
	target = temp_target

#hard clearinig whatever boss card is targetted.
func remove_target():
	if target!= null:
		target.targetted = 0
	target = null

func _process(_delta):
	#The fight button is at the bottom and will tell the player what actions they can take and what will happen if the fight happens.
	if selected.size() == 0 and target==null:
		#A fight can't happen, as no target, no cards picked.
		fight.text = "Pick one target and at least one of your cards"
	elif selected.size()>0 and target==null:
		#Fight can't happen, no boss cards are targetted
		fight.text = "Must select a target"
	elif selected.size()==0 and target!= null:
		#fight can't happen, there is a target, but none of player cards are picked to be used.
		fight.text = "Must select at least one of your cards"
	elif selected.size()>0 and target!= null:
		#Target and player cards are picked, we now will display if combat is allowed or what will happen if combat happens.
		#First, we need to caluclate that using this function
		var check = selected_check()
		match check:
			BATTLE:
				#This is a success, cards will be moved to discard and show up later.
				fight.text = "Attack! Used cards and target will go to discard pile to be reused."
			SACRIFICE:
				#This is the cards are destroyed, good way to get rid of low value cards to either remove a card you don't want to buy, or one you can't beat but is blocking you.
				fight.text = "Sacrifice! Selected cards are lower than target, so all will be removed from game. Only works on low value cards."
			MORE:
				#displays if value is less than target, but only one card picked.
				fight.text = "Value too low and need two cards for sacrifice."
			DEFEAT:
				#You can't sascrifice against values 10+, they are the final bosses and must be defeated. So this is displayed.
				fight.text = "Surrender! You can not sacrifice against the boss cards! Values 10+"
		
#enum {BATTLE, SACRIFICE,DEFEAT,MORE}
#Calculating what the outcom is of battle.
func selected_check():
	var selected_sum
	var targ_val = target.value
	#if only one card picked, use that value.
	if selected.size() == 1:
		selected_sum = selected[0].value
	#If two cards picked, add the value together.
	elif selected.size() ==2:
		selected_sum = selected[0].value + selected[1].value
	
	#Returned if it's less than the targetted boss card and only one card is picked
	if selected.size() == 1 and selected_sum < targ_val:
		return MORE
	#Returned if two cards picked, target is less than 10, and player cards less than target
	elif selected_sum < targ_val and targ_val<10:
		return SACRIFICE
	#Returned if less than a final boss card
	elif selected_sum < targ_val and targ_val>=10:
		return DEFEAT
	#Picked cards will successfully purchase the targetted cards.
	elif selected_sum >= targ_val:
		return BATTLE
	
#If not already done, go to "node" tab and pick "pressed" so this code runs when battle button is pressed.
func _on_fight_pressed() -> void:
	var check = selected_check()
	match check:
		BATTLE:
			#for each selected card
			while selected.size() > 0:
				#Add the value of the selected card to the discard pile
				discard_pile.append(selected[0].value)
				#Have the card be given a value from the deck.
				selected[0].value = player_deck.pop_front()
				#Unselect the card
				remove_selected(selected[0])
			#add the purcahsed/defeated boss card to discard pile
			discard_pile.append(target.value)
			#Give a new value from the boss deck to the defeated boss card.
			target.value = boss_deck.pop_front()
			#clear targetted card.
			remove_target()
		#Sacrifice removes the three cards from the game.
		SACRIFICE:
			#For each selected card, remove them from game and selection arrays
			while selected.size() > 0:
				selected[0].value = player_deck.pop_front()
				remove_selected(selected[0])
			target.value = boss_deck.pop_front()
			remove_target()
	#function to shuffle discard pile into player deck if needed.
	shuffle_discard_into_deck()


func shuffle_discard_into_deck():
	#Shuffles the discard then adds to the bottom of player deck when player deck is about to run out.
	if player_deck.size() < 3:
		discard_pile.shuffle()
		player_deck += discard_pile
	#If the player has sacrificed so many cards that after shuffling their deck is still tiny, we give them 2 free 0 cards.
	#Isn't that so nice of us?
	if player_deck.size() < 3:
		player_deck.append(0)
		player_deck.append(0)

Attach Signal

Set focus to Fight in scene tab (CTRL+F8).

Ctrl+F8 to go to node tab

Tab to "Pressed()" and press enter twice to connect the signal to the on_pressed() function.

Test and Tweak!

With the game scene selected, press F5 to play the game. Then try to adjust it! If it's too easy to win, try tweaking values, or change mechanics! Can make it so perfect values increase card value, or maybe when you sacrifice all Boss cards on the field get higher.

Can make it so maybe if you use a 1 card as part of a capture you get some sort of bonus so they aren't thrown away as soon as possible! Or add sound effects and names and additional values. Since the arrays hold the actual instances, it's very easy to expand what the game checks and does with them!

Thank you for doing the tutorial, have a great day!