Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'rspec'
gem "rspec"
gem "activemodel", require: "active_model"
gem "rubocop"
gem "shoulda-matchers"
34 changes: 34 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
GEM
remote: https://rubygems.org/
specs:
activemodel (5.1.6)
activesupport (= 5.1.6)
activesupport (5.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
ast (2.4.0)
concurrent-ruby (1.0.5)
diff-lcs (1.3)
i18n (1.0.1)
concurrent-ruby (~> 1.0)
minitest (5.11.3)
parallel (1.12.1)
parser (2.5.1.0)
ast (~> 2.4.0)
powerpack (0.1.1)
rainbow (3.0.0)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
Expand All @@ -15,12 +32,29 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.0)
rubocop (0.55.0)
parallel (~> 1.10)
parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.9.0)
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.3.2)

PLATFORMS
ruby

DEPENDENCIES
activemodel
rspec
rubocop
shoulda-matchers

BUNDLED WITH
1.17.1
6 changes: 6 additions & 0 deletions spec/computer_player_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require_relative "../src/computer_player"

RSpec.describe ComputerPlayer, type: :model do
it { should validate_presence_of(:strategy_id) }
it { should validate_inclusion_of(:strategy_id).in_range(1..2) }
end
46 changes: 46 additions & 0 deletions spec/game_board_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require_relative "../src/game_board"

RSpec.describe GameBoard, type: :model do
it { should validate_presence_of(:number_of_columns) }
it { should validate_presence_of(:number_of_rows) }
context "#initialize" do
it "builds the grid from number_of_columns and number_of_rows" do
board = GameBoard.new(number_of_columns: 4, number_of_rows: 3)
expect(board.grid).to eq([[nil, nil, nil, nil],[nil, nil, nil, nil],[nil, nil, nil,nil]])
end
end

context "#empty_row_in_column?" do
it "returns true if a column is nil for at least one row" do
board = GameBoard.new(number_of_columns: 3, number_of_rows: 3)
board.grid = ([["Player", nil, nil],["Player", nil, nil],[nil, nil, nil]])

expect(board.empty_row_in_column?(0)).to be true
end

it "returns false if a column is filled in every column" do
board = GameBoard.new(number_of_columns: 3, number_of_rows: 3)
board.grid = ([["Player", nil, nil],["Player", nil, nil],["Player", nil, nil]])

expect(board.empty_row_in_column?(0)).to be false
end
end

context "#drop_gamepiece" do
it "fills the bottom-most column" do
board = GameBoard.new(number_of_columns: 3, number_of_rows: 3)

board.drop_gamepiece(0, "Player")
expect(board.grid).to eq([[nil, nil, nil],[nil, nil, nil],["Player", nil, nil]])

board.drop_gamepiece(0, "Player")
expect(board.grid).to eq([[nil, nil, nil],["Player", nil, nil],["Player", nil, nil]])
end

it "raises an error if column is already full" do
board = GameBoard.new(number_of_columns: 3, number_of_rows: 3)
board.grid = ([["Player", nil, nil],["Player", nil, nil],["Player", nil, nil]])
expect { board.drop_gamepiece(0, "Player") }.to raise_error(ColumnFullError)
end
end
end
4 changes: 4 additions & 0 deletions spec/human_player_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require_relative "../src/human_player"

RSpec.describe HumanPlayer, type: :model do
end
6 changes: 6 additions & 0 deletions spec/player_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require_relative "../src/player"

RSpec.describe Player, type: :model do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:token) }
end
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "shoulda-matchers"
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
Expand All @@ -17,6 +18,7 @@
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.include(Shoulda::Matchers::ActiveModel, type: :model)
config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
Expand Down
25 changes: 25 additions & 0 deletions spec/validations_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require_relative "../src/validations"

RSpec.describe Validations, "#validate_numerical_input" do
include Validations
it "raises an error for a string as input" do
input = "sdfa"
range = (1..10)
expect{validate_numerical_input(input, range)}.to raise_error(UserInputError)
end
it "raises an error for nil" do
input = nil
range = (1..10)
expect{validate_numerical_input(input, range)}.to raise_error(UserInputError)
end
it "raises an error for a number greater than range" do
input = 11
range = (1..10)
expect{validate_numerical_input(input, range)}.to raise_error(UserInputError)
end
it "raises an error for a number less than range" do
input = 0
range = (1..10)
expect { validate_numerical_input(input, range) }.to raise_error(UserInputError)
end
end
7 changes: 7 additions & 0 deletions src/column_full_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require_relative 'user_input_error'

class ColumnFullError < UserInputError
def initialize(message = "Whoa there, bucco! The column you picked is already full!.")
super(message)
end
end
52 changes: 52 additions & 0 deletions src/computer_player.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "active_model"
require_relative "player"

class ComputerPlayer < Player
include ActiveModel::Validations
attr_accessor :strategy_id, :strategy

validates :strategy_id, presence: true, inclusion: { in: (1..2) }

def initialize(name: "Conan the AI Destroyer", strategy_id: nil)
super
@strategy = set_strategy(strategy_id)
end

def play_turn(board)
column = choose_column(board)
play_gamepiece(column)
end

def choose_column(board)
if strategy == "Random walk"
identify_rows_with_pieces
identify_columns_with_pieces
elsif strategy == "Min-max"
end
end

def set_strategy(strategy_id)
case strategy_id
when 1 then "Random walk"
when 2 then "Min-max"
end
end

private

attr_accessor :columns_with_pieces, :rows_with_pieces

def identify_columns_with_pieces
@columns_with_pieces = []
board.grid.transpose.each_with_index do |column, index|
columns_with_pieces << column if column.include?(token)
end
end

def identify_rows_with_pieces
@rows_with_pieces = []
board.grid.each_with_index do |row, index|
rows_with_pieces << row if row.include?(token)
end
end
end
103 changes: 103 additions & 0 deletions src/game.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
require "forwardable"
require_relative "game_board"
require_relative "computer_player"
require_relative "human_player"
require_relative "system_output"
require_relative "validations"

class Game
extend Forwardable
include Validations
include SystemOutput

attr_reader :board, :number_of_columns
def_delegators :board, :drop_gamepiece, :validate_space_in_column

def initialize
@players = []
end

def run
set_game_dimensions
set_winning_condition
set_players
begin_the_game!
end

private

attr_accessor :players
attr_reader :number_of_columns, :number_of_rows

def greatest_dimension
[number_of_columns, number_of_rows].max
end

def set_game_dimensions
set_column_dimension
set_row_dimension
@board = GameBoard.new(number_of_columns: number_of_columns, number_of_rows: number_of_rows)
end

def set_winning_condition
ask_for_number_of_winning_series_length
begin
input = gets.chomp.to_i
validate_numerical_input(input, (3..greatest_dimension))
@winning_length = input
rescue UserInputError => e
puts e.message
set_winning_condition
end
end

def set_row_dimension
ask_for_number_of_rows
begin
input = gets.chomp.to_i
validate_numerical_input(input, GameBoard::ACCEPTABLE_DIMENSIONS)
@number_of_rows = input
rescue UserInputError => e
puts e.message
set_row_dimension
end
end

def set_column_dimension
ask_for_number_of_columns
begin
input = gets.chomp.to_i
validate_numerical_input(input, GameBoard::ACCEPTABLE_DIMENSIONS)
@number_of_columns = input
rescue UserInputError => e
puts e.message
set_column_dimension
end
end

def begin_the_game!
players.each do |player|
print_grid
player.play_turn
return congratulatory_message(player) if won?(player)
end
end

def set_ai_player
ask_for_ai_strategy
begin
strategy_id = gets.chomp.to_i
validate_numerical_input(strategy_id, (1..2))
players << ComputerPlayer.new(strategy_id: strategy_id)
rescue UserInputError => e
puts e.message
set_ai_player
end
end

def set_players
ask_for_player_name
players << HumanPlayer.new(name: gets.chomp)
set_ai_player
end
end
52 changes: 52 additions & 0 deletions src/game_board.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "active_model"
require_relative "column_full_error"

class GameBoard
include ActiveModel::Validations
ACCEPTABLE_DIMENSIONS = (3..10)
attr_accessor :grid, :number_of_columns, :number_of_rows

validates :grid, presence: true
validates :number_of_columns, presence: true
validates :number_of_rows, presence: true

def initialize(number_of_columns: nil, number_of_rows: nil)
@number_of_columns = number_of_columns
@number_of_rows = number_of_rows
@grid = build_grid(number_of_columns, number_of_rows)
end

def drop_gamepiece(column, player)
validate_space_in_column(column)
mark_first_empty_row_for(column, player)
end

def empty_row_in_column?(column)
grid.each do |row|
return true if row[column].nil?
end
false
end

private

def build_grid(columns, rows)
grid = []
rows&.times do
grid << Array.new(columns)
end
grid
end

def mark_first_empty_row_for(column, player)
reversed_grid = grid.reverse
reversed_grid.each_with_index do |row, index|
return reversed_grid[index][column] = player if row[column].nil?
end
grid = reversed_grid.reverse
end

def validate_space_in_column(column)
raise ColumnFullError unless empty_row_in_column?(column)
end
end
Loading