Created
February 15, 2023 10:58
-
-
Save owenbutler/76ef5b1d374be652aba5b41123168b4f to your computer and use it in GitHub Desktop.
Klondike clone using Dragonruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $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