boardgame_engine 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7129074317f7ea44e65ad18e5952e2401e47be99186e26dd42b0c083ebc91797
4
- data.tar.gz: f87aebcb6502cbfd9d8ee04e60b775f1497e09527863f3f820a1edb202297330
3
+ metadata.gz: 8f3c14dd095359859bb7c6fb53b25e7c94ae18bb1b4e407d0e377797254930a6
4
+ data.tar.gz: 5ecf4d791871a327a1f2e2844cc21b5d0de4b23b001f478a8ef8ca89834bc803
5
5
  SHA512:
6
- metadata.gz: 1c1ab01918757d936b1ee97cf8d75bbf19a300e71450b0c1896fcd9cdda0f804b25324b71ef01929d8d231f96ef7af15ec023690dc52d5a738e8b363da43c4f8
7
- data.tar.gz: b10198cde214fe10e7a861a27419e6235b25a84ef1fac0992de66e0297668da8e3f600effaae0049c12a5343e761dccfb623594bc2621d40b24063e8b8d82cb0
6
+ metadata.gz: 8a8579be5461dda2d3e77a4c2fa50b92ff0a6d4179f960e2ce083261bbc5db6cfe05defa5f1e679e07576bd517d2296477666b86f4388be30030b8127a2ce5ef
7
+ data.tar.gz: afd22e1654d2ca4a66f648e96d08cfd9f52ba3c780d19d4352344214dd6b753ba85236f8e2b0bbf47ecce2c1bd272ac6c33ed6592c0a96838db3c0ea50b327c5
data/.rubocop.yml CHANGED
@@ -1,13 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.1
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
2
+ TargetRubyVersion: 3.0
11
3
 
12
4
  Layout/LineLength:
13
5
  Max: 120
data/Gemfile CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in boardgame_engine.gemspec
6
6
  gemspec
7
7
 
8
- gem "rake", "~> 13.0"
8
+ gem 'rake', '~> 13.0'
9
9
 
10
- gem "rubocop", "~> 1.21"
10
+ gem 'rubocop', '~> 1.21'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boardgame_engine (0.1.0)
4
+ boardgame_engine (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A gem that provides a template for creating boardgames. It aims to streamline
4
4
  the process of making boardgames to be played on the terminal by providing a
5
- variety of methods and classes.
5
+ variety of classes and modules.
6
6
 
7
7
  ## Installation
8
8
 
@@ -15,8 +15,43 @@ If bundler is not being used to manage dependencies, install the gem by executin
15
15
  $ gem install boardgame_engine
16
16
 
17
17
  ## Usage
18
+ ### Initial Setup
19
+ To start making your own boardgame using this gem, your game module needs at least two classes -
20
+ a `Game` class inheriting from `Boardgame` and a `Board` class inheriting `Board`.
21
+ `Game` captures the core gameplay loop/logic and the `Board` captures
22
+ interactions with the board.
18
23
 
19
- TODO: Write usage instructions here
24
+ Depending on the game rules, select child modules from the `Games` and `Boards`
25
+ and include it into the `Game` and `Board` class respectively.
26
+ These modules will handle much of the game and board logic related to the
27
+ game mechanic/rule.
28
+
29
+ For example, if the turns in your game go in a cycle like 1 -> 2 -> 3 -> 1 -> 2 ...
30
+ your `Game` class would look like
31
+ ```ruby
32
+ class Game < BoardgameEngine::Game
33
+ include Games::CyclicalGame
34
+ NUM_PLAYERS = 3
35
+ ...
36
+ end
37
+ ```
38
+ Including this module makes it so that the `BoardgameEngine::Game#change_turn`
39
+ method automatically hands the turn over to the correct player.
40
+
41
+ ### Overriding
42
+ At the current stage of the app, there are methods that necessarily must be
43
+ overridden in the child class.
44
+ `play_turn` must be implemented on _your_ `Game` class, and must contain what happens
45
+ during a single turn (not a round, just a turn).
46
+
47
+ Additionally, the limited number of modules mean that you will probably have to implement some
48
+ methods on your own. I hope to reduce the number of these as the app matures.
49
+ I left yard comments on all module methods, so hopefully overriding won't be too
50
+ difficult.
51
+
52
+ ## Example Games
53
+ Check out `connect4.rb` for a game built with rather minimal overrides and
54
+ `chess.rb` for a game that makes heavy use of customization.
20
55
 
21
56
  ## Development
22
57
 
@@ -26,4 +61,9 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
26
61
 
27
62
  ## Contributing
28
63
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/Forthoney/boardgame_engine.
64
+ Bug reports are welcome on GitHub at https://github.com/Forthoney/boardgame_engine.
65
+ I may not get to them quickly but it will be greatly helpful
66
+
67
+ As the app is under heavy development I will not look deeply into actually
68
+ pulling pull requests (if there were to be any).
69
+ I will definitely consider the ideas proposed in them, so they are still welcome.
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rubocop/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
5
 
6
6
  RuboCop::RakeTask.new
7
7
 
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/boardgame_engine/version"
3
+ require_relative 'lib/boardgame_engine/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "boardgame_engine"
6
+ spec.name = 'boardgame_engine'
7
7
  spec.version = BoardgameEngine::VERSION
8
- spec.authors = ["FortHoney"]
9
- spec.email = ["castlehoneyjung@gmail.com"]
8
+ spec.authors = ['FortHoney']
9
+ spec.email = ['castlehoneyjung@gmail.com']
10
10
 
11
- spec.summary = "A gem that makes digitizing/creating board games to be played on the terminal quick and easy."
12
- spec.homepage = "https://github.com/Forthoney/boardgame_engine"
13
- spec.required_ruby_version = ">= 2.7.0"
11
+ spec.summary = 'A gem that makes digitizing/creating board games to be played on the terminal quick and easy.'
12
+ spec.homepage = 'https://github.com/Forthoney/boardgame_engine'
13
+ spec.required_ruby_version = '>= 3.0.0'
14
14
 
15
15
  # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
16
16
 
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/Forthoney/boardgame_engine"
19
- spec.metadata["changelog_uri"] = "https://github.com/Forthoney/boardgame_engine/blob/main/CHANGELOG.md"
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/Forthoney/boardgame_engine'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/Forthoney/boardgame_engine/blob/main/CHANGELOG.md'
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
22
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -25,11 +25,11 @@ Gem::Specification.new do |spec|
25
25
  (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
26
  end
27
27
  end
28
- spec.bindir = "exe"
28
+ spec.bindir = 'exe'
29
29
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
- spec.require_paths = ["lib"]
30
+ spec.require_paths = ['lib']
31
31
 
32
- spec.add_development_dependency "rspec", "~> 3.2"
32
+ spec.add_development_dependency 'rspec', '~> 3.2'
33
33
 
34
34
  # For more information and examples about making a new gem, check out our
35
35
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Module for different board types
4
+ module Boards
5
+ # Boards laid out in grid format
6
+ module Grid
7
+ # Print a visual representation of the board
8
+ #
9
+ # @param [Boolean] show_row whether to show row labels
10
+ # @param [Boolean] show_col whether to show column labels
11
+ #
12
+ # @return [void]
13
+ def display(show_row: false, show_col: false)
14
+ if show_row
15
+ @board.each_with_index { |row, idx| puts("#{idx} " + format_row(row)) }
16
+ else
17
+ @board.each { |row| puts format_row(row) }
18
+ end
19
+
20
+ return unless show_col
21
+
22
+ column_spacer = show_row ? ' ' : ''
23
+ puts format_col_numbering(column_spacer)
24
+ end
25
+
26
+ # Accessor for getting piece at a location on the board
27
+ #
28
+ # @param [Array<Integer, Integer>] location the coordinates on the grid to
29
+ # get the piece from
30
+ #
31
+ # @return [Piece] the piece at the location
32
+ def get_piece_at(location)
33
+ row, col = location
34
+ @board.dig(row, col)
35
+ end
36
+
37
+ # Setter for setting a piece at a location on the board
38
+ #
39
+ # @param [Array<Integer, Integer>] location <description>
40
+ # @param [Piece] piece the piece to place down
41
+ #
42
+ # @return [void]
43
+ def set_piece_at(location, piece)
44
+ row, col = location
45
+ @board[row][col] = piece
46
+ end
47
+
48
+ # Check whether the given input refers to a valid location on the board,
49
+ # regardless of placement of other pieces, rules, etc
50
+ #
51
+ # @param [String] input the user input
52
+ # @param [Boolean] only_row optional arg for only allowing user to specify
53
+ # row
54
+ # @param [Boolean] only_col optional arg for only allowing user to specify
55
+ # col
56
+ #
57
+ # @return [Boolean] true if the input can be parsed into a location on the
58
+ # board, false otherwise
59
+ def valid_board_input?(input, only_row: false, only_col: false)
60
+ if only_row || only_col
61
+ input.match?(/[0-#{@board.length - 1}]/)
62
+ else
63
+ input.match?(/[0-#{@board.length - 1}], [0-#{@board[0].length - 1}]$/)
64
+ end
65
+ end
66
+
67
+ # Check whether there exists a clear diagonal path from start to a
68
+ # destination
69
+ #
70
+ # @param [Array<Integer, Integer>] start_location the start location
71
+ # @param [Array<Integer, Integer>] end_location the intended destination
72
+ #
73
+ # @return [Boolean] true if there exists an unblocked path to destination
74
+ def clear_diag_path?(start_location, end_location)
75
+ row, col = start_location
76
+ end_row, end_col = end_location
77
+
78
+ ((end_row - row).abs == (end_col - col).abs) \
79
+ && clear_path?(row, col, end_row, end_col, board)
80
+ end
81
+
82
+ # Check whether there exists an unblocked horizontal path from start to
83
+ # a destination
84
+ #
85
+ # @param [Array<Integer, Integer>] start_location the start location
86
+ # @param [Array<Integer, Integer>] end_location the intended destination
87
+ #
88
+ # @return [Boolean] true if there exists an unblocked path to destination
89
+ def clear_horz_path?(start_location, end_location)
90
+ row, col = start_location
91
+ end_row, end_col = end_location
92
+
93
+ (end_row == row) && clear_path?(row, col, end_row, end_col, board)
94
+ end
95
+
96
+ # Check whether there exists an unblocked vertical path from start to
97
+ # a destination
98
+ #
99
+ # @param [Array<Integer, Integer>] start_location the start location
100
+ # @param [Array<Integer, Integer>] end_location the intended destination
101
+ #
102
+ # @return [Boolean] true if there exists an unblocked path to destination
103
+ def clear_vert_path?(start_location, end_location)
104
+ row, col = start_location
105
+ end_row, end_col = end_location
106
+
107
+ (end_col == col) && clear_path?(row, col, end_row, end_col, board)
108
+ end
109
+
110
+ # Parse an input from the user that has a corresponding board location. This
111
+ # must be used in after valid_board_input? has confirmed the input is in
112
+ # the correct format
113
+ #
114
+ # @param [String] input a valid
115
+ #
116
+ # @return [Array<Integer, Integer>]
117
+ def parse_input(input)
118
+ input.split(',').map(&:to_i)
119
+ end
120
+
121
+ # Make a 2D Array representing the board
122
+ #
123
+ # @param [Integer] row the number of rows
124
+ # @param [Integer] col the number of columns
125
+ #
126
+ # @return [Array<Array>] A 2D Array
127
+ def generate_board(row, col)
128
+ Array.new(row) { Array.new(col) { nil } }
129
+ end
130
+
131
+ # Check whether there are consecutive pieces on the board
132
+ #
133
+ # @param [Integer] num the number of consecutive pieces to check for.
134
+ # @param [Boolean] row check for row-wise consecutive pieces
135
+ # @param [Boolean] col check for column-wise consecutive pieces
136
+ # @param [Boolean] diagonal check for diagonally consecutive pieces
137
+ #
138
+ # @return [Boolean] true if any specified directins have num number of
139
+ # consecutive pieces
140
+ def consecutive?(num, row: true, col: true, diagonal: true)
141
+ configs = []
142
+ configs << @board if row
143
+ configs << @board.transpose if col
144
+ configs << align_diagonal(@board) << align_diagonal(@board.transpose) if diagonal
145
+
146
+ configs.each do |board|
147
+ board.each { |array| return true if row_consecutive?(num, array) }
148
+ end
149
+ false
150
+ end
151
+
152
+ private
153
+
154
+ # calculates the location of the next cell in the sequence of cells from a
155
+ # given start location and an end location
156
+ #
157
+ # @param [Integer] row
158
+ # @param [Integer] col
159
+ # @param [Integer] end_row
160
+ # @param [Integer] end_col
161
+ #
162
+ # @return [Array<Integer, Integer>] the next cell in the sequence of cells
163
+ def next_cell(row, col, end_row, end_col)
164
+ row_move = 0
165
+ col_move = 0
166
+
167
+ col_move = (end_col - col) / (end_col - col).abs if end_col != col
168
+ row_move = (end_row - row) / (end_row - row).abs if end_row != row
169
+
170
+ [row + row_move, col + col_move]
171
+ end
172
+
173
+ # Given a linear path, check whether there exists an unblocked path.
174
+ # It msut be checked beforehand that the path is indeed linear
175
+ #
176
+ # @param [Integer] row
177
+ # @param [Integer] col
178
+ # @param [Integer] end_row
179
+ # @param [Integer] end_col
180
+ #
181
+ # @return [Boolean] true if there exists a clear path
182
+ def clear_path?(row, col, end_row, end_col)
183
+ current_tile = get_piece_at([row, col])
184
+
185
+ if (row == end_row) && (col == end_col)
186
+ current_tile.nil? || (current_tile.owner != @owner)
187
+ elsif current_tile.nil? || current_tile.equal?(self)
188
+ next_row, next_col = next_cell(row, col, end_row, end_col)
189
+ clear_path?(next_row, next_col, end_row, end_col)
190
+ else
191
+ false
192
+ end
193
+ end
194
+
195
+ # Check a single row for consecutive pieces
196
+ #
197
+ # @param [Integer] num the number of consecutive pieces to check for
198
+ # @param [Array] array the row to check in
199
+ #
200
+ # @return [Boolean] true if there are at least num number of consecutive
201
+ # elements
202
+ def row_consecutive?(num, array)
203
+ chunked_elems = array.chunk { |elem| elem }
204
+ elem_counts = chunked_elems.map { |elem, elems| [elem, elems.length] }
205
+ elem_counts.any? { |elem, count| count >= num && elem }
206
+ end
207
+
208
+ # Align the diagonals of a board. Must be called once on the original board
209
+ # and once more on the transposed board to check for both directions of
210
+ # diagonals
211
+ #
212
+ # @param [Array<Array>] board the board to align
213
+ #
214
+ # @return [Array<Array>] new board with the diagonals aligned
215
+ def align_diagonal(board)
216
+ board.map.with_index do |row, idx|
217
+ left_filler = Array.new(board.length - 1 - idx, nil)
218
+ right_filler = Array.new(idx, nil)
219
+ left_filler + row + right_filler
220
+ end
221
+ end
222
+
223
+ # Format a single row for String representation
224
+ #
225
+ # @param [Array] row the row to turn into a String
226
+ #
227
+ # @return [String] a String representation of the row
228
+ def format_row(row)
229
+ row.map { |elem| "[#{elem.nil? ? ' ' : elem}]" }.join
230
+ end
231
+
232
+ # Format the column numbering of a board
233
+ #
234
+ # @param [String] column_spacer the spacer to use between the indices
235
+ #
236
+ # @return [String] a String representation of the row
237
+ def format_col_numbering(column_spacer)
238
+ @board[0].each_index.reduce(column_spacer) { |str, idx| str + " #{idx} " }
239
+ end
240
+ end
241
+
242
+ # Boards with movable pieces
243
+ module MovablePiece
244
+ # Move a piece from at a start location to an end location. Should be called
245
+ # after checking that the movement is valid board and game logic wise.
246
+ #
247
+ # @param [<Type>] start_location the starting location of the piece
248
+ # @param [<Type>] end_location the destination of the piece
249
+ # @param [<Type>] set_start_to what to place at the start location. defaults
250
+ # to nil
251
+ #
252
+ # @return [Piece] the piece at the destination
253
+ def move_piece(start_location, end_location, set_start_to: nil)
254
+ piece = get_piece_at(start_location)
255
+ set_piece_at(start_location, set_start_to)
256
+ destination_piece = get_piece_at(end_location)
257
+ set_piece_at(end_location, piece)
258
+
259
+ destination_piece
260
+ end
261
+ end
262
+ end
@@ -2,145 +2,167 @@
2
2
 
3
3
  # All classes in this script are intended to be abstract, meaning they should
4
4
  # not be called on their own.
5
-
6
5
  module BoardgameEngine
7
- # Class representing a physical board comprised of a grid in a board game. It
8
- # acts as both the View and Model if the project were to be compared to a MVC
9
- # model. It plays both roles as the board in a board game not only stores data,
10
- # but also IS the data that must be shown to the players.
11
- class Board
12
- attr_reader :board
13
-
14
- def initialize(row, col)
15
- @board = Array.new(row) { Array.new(col) { nil } }
16
- end
17
-
18
- def display(show_row: false, show_col: false)
19
- if show_row
20
- @board.each_with_index { |row, idx| puts("#{idx} " + format_row(row)) }
21
- else
22
- @board.each { |row| puts format_row(row) }
23
- end
24
- return unless show_col
25
-
26
- column_spacer = show_row ? " " : ""
27
- puts(@board[0].each_index.reduce(column_spacer) do |str, idx|
28
- str + " #{idx} "
29
- end)
30
- end
31
-
32
- def move_piece(start_row, start_col, end_row, end_col)
33
- piece = @board[start_row][start_col]
34
- @board[start_row][start_col] = nil
35
- destination = @board[end_row][end_col]
36
- @board[end_row][end_col] = piece
37
-
38
- destination
39
- end
40
-
41
- private
42
-
43
- def format_row(row)
44
- row.map { |elem| "[#{elem.nil? ? " " : elem}]" }.join
45
- end
46
-
47
- def spot_playable?(piece, row, col)
48
- piece.possible_moves.include? [row, col]
49
- end
50
- end
51
-
52
6
  # Class representing a player in a game
53
7
  class Player
54
8
  attr_reader :name
55
9
 
10
+ # Creates a player object with the given name
11
+ #
12
+ # @param [String] name the name of the player
56
13
  def initialize(name)
57
14
  @name = name
58
15
  end
59
16
 
17
+ # String representation of player
18
+ #
19
+ # @return [String] the name of the player
60
20
  def to_s
61
21
  @name.to_s
62
22
  end
63
23
  end
64
24
 
65
- # Class for running a board game.
66
- class Boardgame
25
+ # Class representing the game loop. It contains the tutorial sequence and
26
+ # information about the core gameplay loop
27
+ class Game
28
+ PLAY_INSTRUCTIONS = ''
67
29
  EXIT_INSTRUCTIONS ||= "Try a sample input or input 'back' to leave the " \
68
30
  "tutorial. Type in 'exit' anytime to exit the game fully"
31
+ GAME_NAME = 'Boardgame'
32
+ NUM_PLAYERS = 2
33
+
34
+ # Begins a round of the Game
35
+ #
36
+ # @param [Boolean] do_onboarding optional argument on whether to do
37
+ # onboarding
38
+ #
39
+ # @return [void]
40
+ def self.start(do_onboarding: true)
41
+ names = []
42
+ self::NUM_PLAYERS.times do |i|
43
+ puts "What is Player #{i}'s name?"
44
+ names.push(gets.chomp)
45
+ end
69
46
 
70
- def initialize(board, instructions, name1 = "Player 1", name2 = "Player 2")
71
- @player1 = Player.new(name1)
72
- @player2 = Player.new(name2)
73
- @board = setup_board(board)
74
- @instructions = instructions
75
- @winner = nil
76
- end
77
-
78
- def self.play(do_onboarding: true)
79
- puts "What is Player 1's name?"
80
- player1 = gets.chomp
81
- puts "What is Player 2's name?"
82
- player2 = gets.chomp
83
- @game = new(player1, player2)
47
+ @game = new(names)
84
48
 
85
49
  puts "Welcome to #{@game}!"
86
50
  @game.onboarding if do_onboarding
87
51
  puts "Starting #{@game}..."
88
- @game.start
89
- end
90
-
91
- def to_s(game_name = "boardgame")
92
- "#{game_name} between #{@player1} and #{@player2}"
52
+ @game.play
93
53
  end
94
54
 
55
+ # Execute onboarding sequence where the player is asked if they want a
56
+ # tutorial
57
+ #
58
+ # @return [void]
95
59
  def onboarding
96
60
  puts "Would you like a tutorial on how to play on this program? \n(y, n)"
61
+
97
62
  case gets.chomp
98
- when "y"
63
+ when 'y'
99
64
  tutorial
100
- when "n"
101
- puts "Skipping tutorial"
65
+ when 'n'
66
+ puts 'Skipping Tutorial'
102
67
  else
103
68
  puts 'Please answer either "y" or "n"'
104
69
  onboarding
105
70
  end
106
71
  end
107
72
 
108
- def tutorial
109
- puts @instructions + Boardgame::EXIT_INSTRUCTIONS
110
- input = gets.chomp
111
- until input == "back"
112
- exit if input == "exit"
113
- puts valid_input?(input) ? "Valid input!" : "Invalid input"
114
- input = gets.chomp
115
- end
116
- end
117
-
118
- def start(turn = @player1)
73
+ # Play the game
74
+ #
75
+ # @param [Player] turn the player who is going first
76
+ #
77
+ # @return [void]
78
+ def play(turn = @players[0])
119
79
  @turn = turn
120
80
  @board.display
121
81
  until @winner
122
82
  play_turn
123
83
  @board.display
84
+ change_turn
124
85
  end
125
86
  puts "#{@winner} wins!"
126
87
  end
127
88
 
89
+ # String representation of the game
90
+ #
91
+ # @return [String] <description>
92
+ def to_s
93
+ "#{self.class::GAME_NAME} between #{@players.join(', ')}"
94
+ end
95
+
128
96
  protected
129
97
 
130
- def proper_format_input(special_commands = [])
98
+ # Constructor for a board game. Kept private so that an object of just class
99
+ # BoardGame cannot be instantiated without being inherited. It essentially
100
+ # keeps it as an abstract class
101
+ #
102
+ # @param [Board] board the board to play on
103
+ # @param [Array<String>] names the names of the players
104
+ def initialize(board, names)
105
+ @players = names.map { |name| Player.new name }
106
+ @board = setup_board(board)
107
+ @winner = nil
108
+ end
109
+
110
+ # Run tutorial for the game
111
+ def tutorial
112
+ puts self.class::PLAY_INSTRUCTIONS + self.class::EXIT_INSTRUCTIONS
113
+ input = gets.chomp
114
+ until input == 'back'
115
+ exit if input == 'exit'
116
+ puts @board.valid_board_input?(input) ? 'Valid input' : 'Invalid input'
117
+ input = gets.chomp
118
+ end
119
+ end
120
+
121
+ # Prompts a user for a board input until a proper input is received
122
+ #
123
+ # @param [Array<String>] special_commands a list of commands that are not
124
+ # valid board input but are valid commands like "back"
125
+ #
126
+ # @return [String] a valid input
127
+ def get_valid_board_input(special_commands = [])
131
128
  input = gets.chomp
132
- until valid_input?(input)
133
- exit if input == "exit"
134
- return input if special_commands.include?(input)
135
129
 
136
- puts "Input is in the wrong format or out of bounds. Try again"
130
+ until @board.valid_board_input?(input) || special_commands.include?(input)
131
+ exit if input == 'exit'
132
+
133
+ puts 'Invalid input. Try again'
137
134
  input = gets.chomp
138
135
  end
136
+
139
137
  input
140
138
  end
141
139
 
140
+ # Setup the board
141
+ #
142
+ # @param [Class] board the class of the board to be used in the game
143
+ #
144
+ # @return [Board] a new board
142
145
  def setup_board(board)
143
146
  board.new
144
147
  end
145
148
  end
149
+
150
+ class Piece
151
+ attr_accessor :status
152
+ attr_reader :owner
153
+
154
+ def initialize(owner, name)
155
+ @status = 'alive'
156
+ @owner = owner
157
+ @name = name
158
+ end
159
+
160
+ def kill(other)
161
+ other.status = 'dead'
162
+ end
163
+
164
+ def to_s
165
+ @name.to_s
166
+ end
167
+ end
146
168
  end