Last active
October 26, 2015 16:53
-
-
Save Sephi-Chan/3569f21cd6cbf5800be2 to your computer and use it in GitHub Desktop.
This file contains 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
# Accepts a data structure such as: | |
# [ [ 28 ], [ 29, 30 ], [ 80 ], [ 6 ] ] | |
class Board < Struct.new(:matrix) | |
Response = Struct.new(:load) | |
# Finds the column (1-indexed) with the nearest predecessor of the given value. | |
# Returns nil if there is no such predecessor. | |
def column_for(given_value) | |
last_values = matrix.map(&:last) | |
distances = last_values.map { |value| distance(given_value - value) } | |
return nil if distances == [ Float::INFINITY, Float::INFINITY, Float::INFINITY, Float::INFINITY ] | |
distances.index(distances.min) + 1 | |
end | |
def add(value, column) | |
matrix[column - 1] << value | |
if matrix[column - 1].size == 6 | |
load = matrix[column - 1][0, 5] | |
matrix[column - 1] = [ value ] | |
Response.new(Load.new(load)) | |
else | |
Response.new(nil) | |
end | |
end | |
def [](column) | |
matrix[column - 1] | |
end | |
private | |
def distance(difference) | |
difference < 0 ? Float::INFINITY : difference | |
end | |
end |
This file contains 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
class RoundRunner | |
class GameNotRunning < StandardError; end | |
class NotAllowedCard < StandardError; end | |
class PlayerAlreadyPlayed < StandardError; end | |
class NoSubstitutionRequired < StandardError; end | |
class SubstitutionForbidden < StandardError; end | |
Response = Struct.new(:game, :round) | |
PickResponse = Struct.new(:game, :round, :resolve_turn) | |
ResolutionResponse = Struct.new(:game, :round, :remaining_turns) | |
RoundEndResponse = Struct.new(:game, :round, :looser) | |
def initialize(game) | |
@game = game | |
end | |
def start(deck) | |
raise GameNotRunning unless @game.running? | |
if round = current_round | |
Response.new(@game, round) | |
else | |
Game.transaction do | |
round = @game.rounds.create | |
dispatch_cards(round, deck.clone) | |
Response.new(@game, round) | |
end | |
end | |
end | |
def pick(player, card_value) | |
round = current_round | |
raise PlayerAlreadyPlayed if player.cards.where(round: round, status: Card.statuses[:picked]).any? | |
raise NotAllowedCard unless card = player.cards.in_hand.find_by(value: card_value) | |
card.picked! | |
sorted_picked_cards = round.cards.picked.order(:value).to_a | |
resolve_turn = @game.maximum_players == round.cards.picked.count | |
PickResponse.new(@game, round, resolve_turn) | |
end | |
def substitute(player, column) | |
round = current_round | |
raise NoSubstitutionRequired unless round.require_substitution? | |
low_card = current_round.cards.order(:value).first | |
raise SubstitutionForbidden unless low_card.player == player | |
round.waiting! | |
round.cards.where(column: column).update_all(status: Card.statuses[:in_load], loaded_player_id: low_card.player_id) | |
low_card.update(column: column, status: Card.statuses[:on_board], played_at: Time.current) | |
Response.new(@game, round) | |
end | |
def resolve_turn | |
round = current_round | |
sorted_picked_cards = round.cards.picked.order(:value).to_a | |
return unless sorted_picked_cards.size == @game.maximum_players | |
board = round.board | |
sorted_picked_cards.each do |card| | |
if column = board.column_for(card.value) | |
response = board.add(card.value, column) | |
if response.load | |
card.update(column: column, played_at: Time.current, status: :on_board) | |
round.cards.where(value: response.load.values).update_all(status: Card.statuses[:in_load], loaded_player_id: card.player_id) | |
else | |
card.update(column: column, played_at: Time.current, status: :on_board) | |
end | |
else | |
round.require_substitution! | |
end | |
end | |
remaining_turns = current_round.cards.in_hand.count / @game.maximum_players | |
ResolutionResponse.new(@game, round, remaining_turns) | |
end | |
def finish_round(deck) | |
round = current_round | |
return unless round.cards.in_hand.count == 0 | |
accumulation = loads_accumulation | |
highest_weight = accumulation.values.max | |
if highest_weight >= @game.load_threshold | |
looser = accumulation.key(accumulation.values.max) | |
@game.finished! | |
round.finished! | |
RoundEndResponse.new(@game, round, looser) | |
else | |
round = @game.rounds.create | |
dispatch_cards(round, deck) | |
RoundEndResponse.new(@game, round, nil) | |
end | |
end | |
private | |
def current_round | |
@game.rounds.where.not(status: Round.statuses[:finished]).last | |
end | |
def dispatch_cards(round, deck) | |
(1..4).each do |column| | |
round.cards.create(value: deck.shift, column: column, status: :on_board) | |
end | |
@game.players.each do |player| | |
@game.cards_per_player.times { round.cards.create(value: deck.shift, player: player, status: :in_hand) } | |
end | |
end | |
def loads_accumulation | |
@game.rounds.map(&:loads).inject({}) do |memo, load| | |
load.each do |player, load_weight| | |
memo[player] ||= 0 | |
memo[player] += load_weight | |
end | |
memo | |
end | |
end | |
end |
This file contains 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
require 'rails_helper' | |
RSpec.describe RoundRunner, type: :model do | |
let(:game_settings) { GameCreator::Settings.new(3, 10, 66) } | |
let(:board_cards) { [ 93, 32, 27, 24 ] } | |
let(:corwin_cards) { [ 10, 31, 82, 3, 16, 8, 35, 73, 72, 46 ] } | |
let(:mandor_cards) { [ 77, 17, 5, 33, 61, 28, 90, 56, 11, 96 ] } | |
let(:eric_cards) { [ 69, 53, 37, 65, 102, 57, 75, 74, 38, 78 ] } | |
let(:deck) { board_cards + corwin_cards + mandor_cards + eric_cards } | |
context 'Game is not started' do | |
before do | |
@game = GameCreator.new(game_settings).create('Corwin').game | |
GameRoom.new(@game).add('Mandor') | |
end | |
it 'does not start a round when the game is not full' do | |
expect { RoundRunner.new(@game).start(deck) }.to raise_error(RoundRunner::GameNotRunning) | |
end | |
it 'starts a round when the game is full' do | |
GameRoom.new(@game).add('Eric') | |
GameRoom.new(@game).start | |
RoundRunner.new(@game).start(deck) | |
expect(Game.running.count).to eq(1) | |
expect(@game).to be_running | |
expect(@game.rounds.count).to eq(1) | |
end | |
it 'does not start a new round if a round is already running' do | |
other_response = GameRoom.new(@game).add('Eric') | |
GameRoom.new(other_response.game).start | |
RoundRunner.new(@game).start(deck) | |
RoundRunner.new(@game).start(deck) | |
expect(@game.rounds.count).to eq(1) | |
end | |
end | |
context 'Game is started' do | |
before do | |
@game = GameCreator.new(game_settings).create('Corwin').game | |
GameRoom.new(@game).add('Mandor') | |
GameRoom.new(@game).add('Eric') | |
GameRoom.new(@game).start | |
@response = RoundRunner.new(@game).start(deck) | |
@corwin, @mandor, @eric = @response.game.players | |
end | |
it 'dispatches cards to players and on the board' do | |
expect(@response.round.cards.count).to eq(34) | |
expect(@corwin.cards.in_hand.count).to eq(10) | |
expect(@mandor.cards.in_hand.count).to eq(10) | |
expect(@eric.cards.in_hand.count).to eq(10) | |
expect(@response.round.cards.where(column: 1).count).to eq(1) | |
expect(@response.round.cards.where(column: 2).count).to eq(1) | |
expect(@response.round.cards.where(column: 3).count).to eq(1) | |
expect(@response.round.cards.where(column: 4).count).to eq(1) | |
expect(@response.round.board).to eq(Board.new([ [ 93 ], [ 32 ], [ 27 ], [ 24 ] ])) | |
end | |
it 'allows the player to pick a card from his hand' do | |
pick_response = RoundRunner.new(@game).pick(@corwin, corwin_cards[0]) | |
expect(pick_response.resolve_turn).to be(false) | |
expect(@corwin.cards.first).to be_picked | |
expect(@corwin.cards.in_hand.count).to eq(9) | |
end | |
it 'does not allow the player to pick a card which is not in his hand' do | |
expect { RoundRunner.new(@game).pick(@corwin, mandor_cards[0]) }.to raise_error(RoundRunner::NotAllowedCard) | |
end | |
it 'prevents the player from playing multiple times' do | |
RoundRunner.new(@game).pick(@corwin, corwin_cards[0]) | |
expect { RoundRunner.new(@game).pick(@corwin, corwin_cards[0]) }.to raise_error(RoundRunner::PlayerAlreadyPlayed) | |
end | |
context 'No substitution required' do | |
it 'resolves the turn with no load since all cards fit on the board' do | |
RoundRunner.new(@game).pick(@corwin, corwin_cards[6]) # 35 | |
RoundRunner.new(@game).pick(@mandor, mandor_cards[3]) # 33 | |
last_pick_response = RoundRunner.new(@game).pick(@eric, eric_cards[2]) # 37 | |
resolution_response = RoundRunner.new(@game).resolve_turn | |
expect(last_pick_response.resolve_turn).to be(true) | |
expect(resolution_response.remaining_turns).to eq(9) | |
expect(resolution_response.round.cards.on_board.count).to eq(7) | |
expect(resolution_response.round.board).to eq(Board.new([ | |
[ 93 ], | |
[ 32, 33, 35, 37 ], | |
[ 27 ], | |
[ 24 ] | |
])) | |
end | |
it 'resolves the turn with a load since a card does not fit on the board' do | |
RoundRunner.new(@game).pick(@corwin, corwin_cards[6]) # 35 | |
RoundRunner.new(@game).pick(@mandor, mandor_cards[3]) # 33 | |
RoundRunner.new(@game).pick(@eric, eric_cards[2]) # 37 | |
RoundRunner.new(@game).resolve_turn | |
RoundRunner.new(@game).pick(@corwin, corwin_cards[9]) # 46 | |
RoundRunner.new(@game).pick(@mandor, mandor_cards[0]) # 77 | |
RoundRunner.new(@game).pick(@eric, eric_cards[8]) # 38 | |
RoundRunner.new(@game).resolve_turn | |
expect(@corwin.cards.in_hand.count).to eq(8) | |
expect(@mandor.cards.in_hand.count).to eq(8) | |
expect(@eric.cards.in_hand.count).to eq(8) | |
expect(@response.round.cards.on_board.count).to eq(5) | |
expect(@corwin.cards_in_load.pluck(:value)).to eq([ 32, 33, 35, 37, 38 ]) | |
expect(@response.round.board).to eq(Board.new([ | |
[ 93 ], | |
[ 46, 77 ], | |
[ 27 ], | |
[ 24 ] | |
])) | |
end | |
it 'does not allow substitution' do | |
expect { RoundRunner.new(@game).substitute(@eric, 1) }.to raise_error(RoundRunner::NoSubstitutionRequired) | |
end | |
end | |
context 'Game requires a substitution' do | |
before do | |
RoundRunner.new(@game).pick(@corwin, corwin_cards[3]) # 3 | |
RoundRunner.new(@game).pick(@mandor, mandor_cards[3]) # 33 | |
RoundRunner.new(@game).pick(@eric, eric_cards[2]) # 37 | |
@resolution_response = RoundRunner.new(@game).resolve_turn | |
end | |
it 'requires a substitution since Corwin picked a very low card' do | |
expect(@resolution_response.round.require_substitution?).to be(true) | |
end | |
it 'does not allow other players to substitute' do | |
expect { RoundRunner.new(@game).substitute(@eric, 1) }.to raise_error(RoundRunner::SubstitutionForbidden) | |
end | |
it 'allows Corwin to substitute and replace a column' do | |
response = RoundRunner.new(@game).substitute(@corwin, 1) | |
expect(@corwin.cards_in_load.pluck(:value)).to eq([ 93 ]) | |
expect(response.round.waiting?).to be(true) | |
expect(response.round.board).to eq(Board.new([ | |
[ 3 ], | |
[ 32, 33, 37 ], | |
[ 27 ], | |
[ 24 ] | |
])) | |
end | |
end | |
end | |
it 'dispatches 4 cards to each player' do | |
game_settings = GameCreator::Settings.new(3, 4, 66) | |
game = GameCreator.new(game_settings).create('Corwin').game | |
GameRoom.new(game).add('Mandor') | |
GameRoom.new(game).add('Eric') | |
GameRoom.new(game).start | |
response = RoundRunner.new(game).start(deck) | |
corwin, mandor, eric = response.game.players | |
expect(corwin.cards.in_hand.count).to eq(4) | |
expect(mandor.cards.in_hand.count).to eq(4) | |
expect(eric.cards.in_hand.count).to eq(4) | |
end | |
describe 'Full game' do | |
it 'starts a new round when all cards are played and ends when the load threshold is reached' do | |
board_cards_1 = [ 93, 32, 27, 24 ] | |
corwin_cards_1 = [ 34, 69 ] | |
mandor_cards_1 = [ 53, 17 ] | |
eric_cards_1 = [ 41, 77 ] | |
deck_1 = board_cards_1 + corwin_cards_1 + mandor_cards_1 + eric_cards_1 | |
board_cards_2 = [ 52, 53, 99, 88 ] | |
corwin_cards_2 = [ 55, 78 ] | |
mandor_cards_2 = [ 59, 97 ] | |
eric_cards_2 = [ 60, 80 ] | |
deck_2 = board_cards_2 + corwin_cards_2 + mandor_cards_2 + eric_cards_2 | |
game_settings = GameCreator::Settings.new(3, 2, 15) | |
game = GameCreator.new(game_settings).create('Corwin').game | |
GameRoom.new(game).add('Mandor') | |
GameRoom.new(game).add('Eric') | |
GameRoom.new(game).start | |
start_response = RoundRunner.new(game).start(deck_1) | |
corwin, mandor, eric = start_response.game.players | |
# Board: 93, 32, 27, 24. | |
RoundRunner.new(game).pick(corwin, corwin_cards_1[0]) # 34. | |
RoundRunner.new(game).pick(mandor, mandor_cards_1[0]) # 53. | |
RoundRunner.new(game).pick(eric, eric_cards_1[0]) # 41. | |
RoundRunner.new(game).resolve_turn | |
RoundRunner.new(game).pick(corwin, corwin_cards_1[1]) # 69. | |
RoundRunner.new(game).pick(mandor, mandor_cards_1[1]) # 17. | |
RoundRunner.new(game).pick(eric, eric_cards_1[1]) # 77. | |
resolution_response = RoundRunner.new(game).resolve_turn | |
end_response = RoundRunner.new(game).finish_round(deck_2) | |
expect(resolution_response.round.loads).to eq({ corwin => 0, mandor => 0, eric => 5 }) # Weight for load 32, 34, 41, 53, 69. | |
expect(resolution_response.remaining_turns).to eq(0) | |
expect(corwin.cards.in_hand.count).to eq(2) | |
expect(mandor.cards.in_hand.count).to eq(2) | |
expect(eric.cards.in_hand.count).to eq(2) | |
expect(end_response.game.rounds.count).to eq(2) | |
# Board: 52, 53, 99, 88. | |
RoundRunner.new(game).pick(corwin, corwin_cards_2[0]) # 55. | |
RoundRunner.new(game).pick(mandor, mandor_cards_2[0]) # 59. | |
RoundRunner.new(game).pick(eric, eric_cards_2[0]) # 60. | |
RoundRunner.new(game).resolve_turn | |
RoundRunner.new(game).pick(corwin, corwin_cards_2[1]) # 78. | |
RoundRunner.new(game).pick(mandor, mandor_cards_2[1]) # 97. | |
RoundRunner.new(game).pick(eric, eric_cards_2[1]) # 80. | |
resolution_response_2 = RoundRunner.new(game).resolve_turn | |
end_response_2 = RoundRunner.new(game).finish_round(deck_2.reverse) | |
expect(resolution_response_2.round.loads).to eq({ corwin => 0, mandor => 0, eric => 13 }) # Weight for load 53, 55, 59, 60, 78. | |
expect(end_response_2.looser).to eq(eric) | |
expect(end_response_2.game).to be_finished | |
expect(end_response_2.round).to be_finished | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment