Skip to content

Instantly share code, notes, and snippets.

@Sephi-Chan
Last active October 26, 2015 16:53
Show Gist options
  • Save Sephi-Chan/3569f21cd6cbf5800be2 to your computer and use it in GitHub Desktop.
Save Sephi-Chan/3569f21cd6cbf5800be2 to your computer and use it in GitHub Desktop.
# 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
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
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