Skip to content

Instantly share code, notes, and snippets.

@owenbutler
Created February 15, 2023 10:58
Show Gist options
  • Select an option

  • Save owenbutler/76ef5b1d374be652aba5b41123168b4f to your computer and use it in GitHub Desktop.

Select an option

Save owenbutler/76ef5b1d374be652aba5b41123168b4f to your computer and use it in GitHub Desktop.
Klondike clone using Dragonruby
$deck = []
$game = {}
$card_width = 100
$card_height = 145
$space_between_cards = 156
$waste_and_foundation_y = 560
$waste_x_stagger = 3
$waste_number_of_cards = 30
$tableau_y = 400
$left_margin = 120
$card_stagger_height = 30
$back_facing_card_stagger_height = 10
$double_click_timeout = 30
$no_cards_moved_full_deck = false
$moved_card_this_deck = false
$bg_music = [
{
input: 'sounds/music/bg1-64.ogg',
x: 0.0, y: 0.0, z: 0.0,
gain: 0.2,
pitch: 1.0,
paused: false,
looping: false,
},
{
input: 'sounds/music/bg2-64.ogg',
x: 0.0, y: 0.0, z: 0.0,
gain: 0.2,
pitch: 1.0,
paused: false,
looping: false,
},
]
$empty_card = 'sprites/svg-cards/card_empty.png'
$back_card = 'sprites/svg-cards/card_back.png'
def check_audio args
return if args.audio[:bg]
args.audio[:bg] = $bg_music.sample
end
def moved_card
$moved_card_this_deck = true
end
def tick args
if args.tick_count == 0
init_game args
end
update_card_locations args
# render a background color
args.outputs.background_color = [8, 163, 98]
# send the sprites to the screen
args.outputs.sprites << $game.tableau
args.outputs.sprites << $game.foundations
args.outputs.sprites << $game.waste
args.outputs.sprites << $game.deal_deck
args.outputs.sprites << $game.mouse_cards
handle_input args
# check if it's game over
if game_over
args.outputs.labels << {
x: 20,
y: 37,
text: "Congratulations! You win!",
}
end
check_audio args
check_restart args
end
def handle_input args
downclick = args.inputs.mouse.down
if downclick
handle_downclick downclick, args
end
upclick = args.inputs.mouse.up
if upclick && $game.mouse_cards.length != 0
handle_upclick upclick, args
end
end
def check_restart args
restart_button = { x: 1240, y: 14, w: 24, h: 24, path: 'sprites/restart.png' }
args.outputs.sprites << restart_button
if should_hint_restart?() || args.inputs.mouse.inside_rect?(restart_button)
args.outputs.labels << {
x: 1150,
y: 37,
text: "Restart?",
}
end
click = args.inputs.mouse.click
if click
if click.inside_rect? restart_button
init_game args
end
end
end
def should_hint_restart?
return ($no_cards_moved_full_deck && $moved_card_this_deck == false) || game_over
end
def game_over
$game.foundations[0].last.number == 13 && $game.foundations[1].last.number == 13 && $game.foundations[2].last.number == 13 && $game.foundations[3].last.number == 13
end
def update_card_locations args
# render the tableau:
# walk through the columns, setting x and y on the arrays
# then should be able to simply push the arrays to outputs
$game.tableau.each_with_index do |column, index|
column.each_with_index do |card, card_index|
# set the cards x spot, add space between each column
card.x = $left_margin + index * $space_between_cards
# set the cards x spot to the tableau top point if it's the first or second card
if card_index == 0 || card_index == 1
card.y = $tableau_y
else
card_height_spacer = $card_stagger_height
card_height_spacer = $back_facing_card_stagger_height if column[card_index - 1].path == $back_card
card.y = column[card_index - 1].y - card_height_spacer
end
card.source_col = nil
card.source_foundation = nil
card.from_waste = false
end
end
# set the location of the foundation cards
$game.foundations.each_with_index do |foundation, foundation_index|
foundation.each_with_index do |card, card_index|
card.x = $left_margin + $space_between_cards * 3 + foundation_index * $space_between_cards
card.y = $waste_and_foundation_y
card.source_col = nil
card.source_foundation = nil
card.from_waste = false
end
end
# set the location of the waste cards
$game.waste.each_with_index do |card, index|
card.x = $left_margin + $space_between_cards + (index % $waste_number_of_cards) * $waste_x_stagger
card.y = $waste_and_foundation_y
card.source_col = nil
card.source_foundation = nil
card.from_waste = false
end
# set the location of the picked up cards
$game.mouse_cards.each_with_index do |card, index|
card.x = args.inputs.mouse.x + $game.mouse_click_offset_x
card.y = args.inputs.mouse.y + $game.mouse_click_offset_y - (index * $card_stagger_height)
end
end
def handle_downclick downclick, args
# check if clicking on the deal deck
if downclick.inside_rect? $game.deal_deck[0]
# return if there are no more cards in waste or deck
return if $game.stock.empty? && $game.waste.empty?
# check if the stock is empty
if $game.stock.empty?
$game.stock = $game.waste.reverse
$game.waste = []
$no_cards_moved_full_deck = !$moved_card_this_deck
$moved_card_this_deck = false
$game.deal_deck[0].path = $back_card
end
# deal a card from the deal deck to waste
card = $game.stock.pop
$game.waste.push(card)
args.outputs.sounds << 'sounds/card_putdown.wav'
# if the stock is empty, set the deal deck to be empty
if $game.stock.empty?
$game.deal_deck[0].path = $empty_card
end
return # no point checking the other things
end
# generate list of clickable rects in order
# each face card, plus any cards in a valid stack from the bottom of a column starting at those face cards
valid_clickables = []
$game.tableau.each do |col|
next if col.length == 1
# walk backwards through the column, adding each valid link in a chain to be clickable
(col.length - 1).downto(1) do |index|
clickable = col[index]
next_clickable = col[index - 1]
clickable.source_col = col
clickable.source_index = index
valid_clickables << clickable
# break out if the next card down isn't a valid chain/stack
if clickable.color != next_clickable.color && next_clickable.number && clickable.number == next_clickable.number - 1 && next_clickable.path != $back_card
next
else
break
end
end
end
# add the top waste card if there is one
if $game.waste.length != 0
clickable = $game.waste.last
clickable.from_waste = true
valid_clickables << clickable
end
valid_clickables.each do |clickable|
if downclick.inside_rect? clickable
args.outputs.sounds << 'sounds/card_pickup.wav'
mouse_click_offset(clickable.x - downclick.x, clickable.y - downclick.y)
if valid_double_click clickable
process_double_click clickable
break
end
$game.last_click = {
target: clickable,
time: args.tick_count,
}
# check if the click was on the waste card
if clickable.from_waste
# pick it up
waste_card = $game.waste.pop
waste_card.from_waste = true
$game.mouse_cards.push waste_card
break
end
# else it's from a column click
# pick up all cards from the index of the clicked one, to the end
cards_to_pick_up = clickable.source_col[clickable.source_index..clickable.source_col.length-1]
cards_to_pick_up.each do |card|
card.source_col = clickable.source_col
clickable.source_col.pop
$game.mouse_cards.push card
end
# picked_up_card = clickable.source_col.pop
# picked_up_card.source_col = clickable.source_col
# $game.mouse_cards.push picked_up_card
break
end
end
end
def valid_double_click clicked
$game.last_click && $game.last_click.target == clicked && $game.last_click.time + $double_click_timeout > Kernel::tick_count
end
def process_double_click clicked
puts "valid double click on #{clicked}"
$game.foundations.each do |foundation|
# special case if it's the ace, since it just has to find an empty spot
if clicked.number == 1
next if foundation.length > 1
else
# it's not an ace
# if the foundation is "empty" move on
next if foundation.length == 1
# if the suit doesn't match, move on
next if foundation.last.suit != clicked.suit
# if the number doesn't increment, move on
next if foundation.last.number != clicked.number - 1
end
# if we got to here, it's the right suit and number, move it
# put it on the foundation
foundation.push clicked
$gtk.args.outputs.sounds << "sounds/foundation#{clicked.number}.wav"
# if it was from waste pile, remove it there
if clicked.from_waste
$game.waste.pop
elsif clicked.source_col # else move it from the source column
clicked.source_col.pop
flip_last_card_in_column clicked.source_col
end
break
end
end
def handle_upclick upclick, args
return if $game.mouse_cards.empty?
# generate list of clickable rects in order
# each face card, plus any foundation stack
valid_clickables = []
$game.tableau.each do |col|
next if col.empty?
clickable = col.last
clickable.source_col = col
valid_clickables << clickable
end
$game.foundations.each do |foundation|
clickable = foundation.last
clickable.source_foundation = foundation
valid_clickables << clickable
end
dropped_card = $game.mouse_cards.first
valid_clickables.each do |clicker|
if args.geometry.intersect_rect?(dropped_card, clicker)
# if upclick.inside_rect? clicker
# check if it's a foundation and a single card drop
if clicker.source_foundation && $game.mouse_cards.length == 1
# it's a foundation, check if the foundation is free and the dropped card is an ace only
if clicker.path == $empty_card && $game.mouse_cards.length == 1 && $game.mouse_cards[0].number == 1
# drop the ace into the foundation slot
add_mouse_cards_to_col clicker.source_foundation
$gtk.args.outputs.sounds << 'sounds/foundation1.wav'
break
elsif clicker.suit == $game.mouse_cards[0].suit && clicker.number == $game.mouse_cards[0].number - 1
sound_num = $game.mouse_cards[0].number < 7 ? $game.mouse_cards[0].number : 6
$gtk.args.outputs.sounds << "sounds/foundation#{sound_num}.wav"
# else check if it's the correct suit and increment
add_mouse_cards_to_col clicker.source_foundation
break
end
end
# else check if it's valid to drop on the column
if clicker.source_col
# if the col is empty and we are dropping a king, let it happen
if clicker.path == $empty_card && $game.mouse_cards[0].number == 13
add_mouse_cards_to_col clicker.source_col
break
elsif clicker.color != $game.mouse_cards[0].color && clicker.number == $game.mouse_cards[0].number + 1
# check if the suit alternates the the number decrements
add_mouse_cards_to_col clicker.source_col
break
end
end
end
end
# if we get to here and there is still cards on the mouse, we should put them back where they came
return_mouse_cards
end
def add_mouse_cards_to_col col
# grab the source col of the mouse cards
source_col = $game.mouse_cards.last.source_col
$game.mouse_cards.each do |card|
col.push card
end
$game.mouse_cards.clear
# flip the last card in the source column (if there is a card there)
flip_last_card_in_column source_col
# if source_col
# last_card_in_from_col = source_col.last
# last_card_in_from_col.path = last_card_in_from_col.truepath if source_col.length > 1
# end
$gtk.args.outputs.sounds << 'sounds/card_putdown.wav'
moved_card
end
def flip_last_card_in_column column
if column
last_card_in_from_col = column.last
last_card_in_from_col.path = last_card_in_from_col.truepath if column.length > 1
end
end
def return_mouse_cards
return if $game.mouse_cards.empty?
$gtk.args.outputs.sounds << 'sounds/putdown_alt.wav'
return_card = $game.mouse_cards[0]
if return_card.from_waste
$game.waste.push $game.mouse_cards.pop
elsif return_card.source_col
return_col = return_card.source_col
$game.mouse_cards.each do |card|
return_col.push card
end
$game.mouse_cards.clear
end
end
def mouse_click_offset x, y
$game.mouse_click_offset_x = x
$game.mouse_click_offset_y = y
end
def init_game args
generate_deck
$game.tableau = Array.new(7) { [] }
$game.foundations = Array.new(4) { [] }
$game.waste = []
$game.deal_deck = []
$game.mouse_cards = []
mouse_click_offset(0, 0)
# shuffle the deck
$game.stock = $deck.shuffle
4.times { $game.stock.shuffle! }
# add empty cards to the base of the tableau columns
$game.tableau.each do |f|
f << {x: 0, y: 0, w: $card_width, h: $card_height, path: $empty_card}
end
# deal from the stock to the columns in the tableau
rover = 0
while rover < 7
inner_rover = rover
while inner_rover < 7
card = $game.stock.pop
$game.tableau[inner_rover].push(card)
inner_rover += 1
end
rover += 1
end
# flip the cards at the end of the columns
$game.tableau.each do |col|
last_card = col.last
last_card.path = last_card.truepath
end
$game.stock.each do |card|
card.path = card.truepath
end
# add empty cards to the foundation
$game.foundations.each do |f|
f << {x: 0, y: 0, w: $card_width, h: $card_height, path: $empty_card}
end
# add a back card to the waste pile
$game.deal_deck << {x: $left_margin, y: $waste_and_foundation_y, w: $card_width, h: $card_height, path: $back_card}
end
def generate_deck
suits = [:diamonds, :clubs, :hearts, :spades]
card_values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
$deck = suits.product(card_values).zip.map do |pair|
suit = pair[0][0]
num = pair[0][1]
{
suit: suit,
number: num,
color: get_color(suit),
truepath: "sprites/png-cards-1.3/#{suit}_#{get_path_num(num)}.png",
# truepath: "sprites/svg-cards/#{num}_of_#{suit}.svg.png",
path: $back_card,
w: $card_width,
h: $card_height,
x: 0,
y: 0,
}
end
end
def get_color suit
return :red if suit == :diamonds || suit == :hearts
return :black
end
def get_path_num num
if num < 10
return "0#{num}"
end
num.to_s
end
$gtk.reset
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment