From 42920a9b69a7823ad6e4ebb462bf008d0ebd62a1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 17 Jan 2019 17:25:03 -0600 Subject: [PATCH] in progress --- Gemfile | 5 +- Gemfile.lock | 34 ++++++++++++ spec/computer_player_spec.rb | 6 ++ spec/game_board_spec.rb | 46 ++++++++++++++++ spec/human_player_spec.rb | 4 ++ spec/player_spec.rb | 6 ++ spec/spec_helper.rb | 2 + spec/validations_spec.rb | 25 +++++++++ src/column_full_error.rb | 7 +++ src/computer_player.rb | 52 ++++++++++++++++++ src/game.rb | 103 +++++++++++++++++++++++++++++++++++ src/game_board.rb | 52 ++++++++++++++++++ src/human_player.rb | 13 +++++ src/player.rb | 19 +++++++ src/run.rb | 3 + src/system_output.rb | 85 +++++++++++++++++++++++++++++ src/user_input_error.rb | 5 ++ src/validations.rb | 7 +++ 18 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 spec/computer_player_spec.rb create mode 100644 spec/game_board_spec.rb create mode 100644 spec/human_player_spec.rb create mode 100644 spec/player_spec.rb create mode 100644 spec/validations_spec.rb create mode 100644 src/column_full_error.rb create mode 100644 src/computer_player.rb create mode 100644 src/game.rb create mode 100644 src/game_board.rb create mode 100644 src/human_player.rb create mode 100644 src/player.rb create mode 100644 src/run.rb create mode 100644 src/system_output.rb create mode 100644 src/user_input_error.rb create mode 100644 src/validations.rb diff --git a/Gemfile b/Gemfile index 3b69346..59ae696 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/Gemfile.lock b/Gemfile.lock index a68c5d6..436b8c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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 diff --git a/spec/computer_player_spec.rb b/spec/computer_player_spec.rb new file mode 100644 index 0000000..f75f50a --- /dev/null +++ b/spec/computer_player_spec.rb @@ -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 diff --git a/spec/game_board_spec.rb b/spec/game_board_spec.rb new file mode 100644 index 0000000..be4dd45 --- /dev/null +++ b/spec/game_board_spec.rb @@ -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 diff --git a/spec/human_player_spec.rb b/spec/human_player_spec.rb new file mode 100644 index 0000000..af517ea --- /dev/null +++ b/spec/human_player_spec.rb @@ -0,0 +1,4 @@ +require_relative "../src/human_player" + +RSpec.describe HumanPlayer, type: :model do +end diff --git a/spec/player_spec.rb b/spec/player_spec.rb new file mode 100644 index 0000000..f02b55c --- /dev/null +++ b/spec/player_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 251aa51..02fe5a8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 @@ -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 diff --git a/spec/validations_spec.rb b/spec/validations_spec.rb new file mode 100644 index 0000000..790f152 --- /dev/null +++ b/spec/validations_spec.rb @@ -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 diff --git a/src/column_full_error.rb b/src/column_full_error.rb new file mode 100644 index 0000000..9172f85 --- /dev/null +++ b/src/column_full_error.rb @@ -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 diff --git a/src/computer_player.rb b/src/computer_player.rb new file mode 100644 index 0000000..670d2ac --- /dev/null +++ b/src/computer_player.rb @@ -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 diff --git a/src/game.rb b/src/game.rb new file mode 100644 index 0000000..a2a3c8b --- /dev/null +++ b/src/game.rb @@ -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 diff --git a/src/game_board.rb b/src/game_board.rb new file mode 100644 index 0000000..6819749 --- /dev/null +++ b/src/game_board.rb @@ -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 diff --git a/src/human_player.rb b/src/human_player.rb new file mode 100644 index 0000000..354fd98 --- /dev/null +++ b/src/human_player.rb @@ -0,0 +1,13 @@ +class HumanPlayer < Player + def play_turn(board) + ask_for_a_column + begin + column = gets.chomp.to_i + validate_numerical_input(input: column, range: (1..number_of_columns)) + play_gamepiece(column) + rescue UserInputError => e + puts e.message + play_turn + end + end +end diff --git a/src/player.rb b/src/player.rb new file mode 100644 index 0000000..9503b37 --- /dev/null +++ b/src/player.rb @@ -0,0 +1,19 @@ +require "active_model" + +class Player + include ActiveModel::Validations + + attr_accessor :name, :token + + validates_presence_of :name, :token + + def initialize(name: nil, **args) + @name = name + @token = name[0] + end + + def play_gamepiece(column_number) + column_index = column_number - 1 + drop_gamepiece(column_index, token) + end +end diff --git a/src/run.rb b/src/run.rb new file mode 100644 index 0000000..7273623 --- /dev/null +++ b/src/run.rb @@ -0,0 +1,3 @@ +require_relative "game" + +Game.new.run diff --git a/src/system_output.rb b/src/system_output.rb new file mode 100644 index 0000000..431b9c3 --- /dev/null +++ b/src/system_output.rb @@ -0,0 +1,85 @@ +module SystemOutput + def ask_for_a_column + puts "Choose a column (1 through #{number_of_columns}, from left to right) to drop your token." + end + + def ask_for_ai_strategy + puts "Which AI strategy do you want to play against? Choose 1 for Random Walk, or 2 for Min-Max." + end + + def ask_for_number_of_columns + puts "Choose a number of columns (from 3 to 10) for the grid." + end + + def ask_for_number_of_rows + puts "Choose the number of rows (from 3 to 10) for the grid." + end + + def ask_for_number_of_winning_series_length + puts "How many tokens need to be adjacent to win? (What's the winning length of consecutive tokens, in a row, in a column or diagonally?)" + end + + def ask_for_player_name + puts "What shall we call you?" + end + + def print_congratulatory_message(player) + puts "Congratulations, #{player.name} — victory is yours." + end + + def print_grid + system "clear" + puts "\n" + print_game_title + puts "\n" + print_game_grid + puts "\n" + end + + private + + def print_game_title + puts " #############################" + puts " # #" + puts " # LIFE IS AWESOME: #" + puts " # CONNECT SOME STUFF #" + puts " # #" + puts " #############################" + end + + def print_game_grid + print_header + print_rows + end + + def print_header + header = "|" + number_of_columns.times do |i| + i += 1 + header << " #{i} " + header << "|" + end + header << add_separator(header.length) + puts header + end + + def add_separator(length) + separator = "\n" + length.times do + separator << "—" + end + separator + end + + def print_rows + formatted_rows = "" + board.grid.each_with_index do |row, index| + formatted_row = index > 0 ? "\n| " : "| " + formatted_row << row.map { |e| e.nil? ? " " : e }.join(" | ") + formatted_row << " |" + formatted_rows << formatted_row + formatted_rows << add_separator(formatted_row.length) + end + puts formatted_rows + end +end diff --git a/src/user_input_error.rb b/src/user_input_error.rb new file mode 100644 index 0000000..0f26634 --- /dev/null +++ b/src/user_input_error.rb @@ -0,0 +1,5 @@ +class UserInputError < RuntimeError + def initialize(message = "Whoa there, bucco! You must have mistyped something.") + super(message) + end +end diff --git a/src/validations.rb b/src/validations.rb new file mode 100644 index 0000000..0758c99 --- /dev/null +++ b/src/validations.rb @@ -0,0 +1,7 @@ +require_relative "user_input_error" + +module Validations + def validate_numerical_input(input, range) + raise UserInputError unless range.include?(input) + end +end