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 +4 -4
- data/.rubocop.yml +1 -9
- data/Gemfile +3 -3
- data/Gemfile.lock +1 -1
- data/README.md +43 -3
- data/Rakefile +2 -2
- data/boardgame_engine.gemspec +13 -13
- data/lib/boardgame_engine/board_modules.rb +262 -0
- data/lib/boardgame_engine/boardgame.rb +108 -86
- data/lib/boardgame_engine/chess.rb +135 -166
- data/lib/boardgame_engine/connect4.rb +41 -55
- data/lib/boardgame_engine/game_modules.rb +65 -0
- data/lib/boardgame_engine/sample_games.rb +4 -2
- data/lib/boardgame_engine/version.rb +1 -1
- data/lib/boardgame_engine.rb +6 -8
- metadata +10 -9
- data/lib/boardgame_engine/multiplayergame.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f3c14dd095359859bb7c6fb53b25e7c94ae18bb1b4e407d0e377797254930a6
|
4
|
+
data.tar.gz: 5ecf4d791871a327a1f2e2844cc21b5d0de4b23b001f478a8ef8ca89834bc803
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a8579be5461dda2d3e77a4c2fa50b92ff0a6d4179f960e2ce083261bbc5db6cfe05defa5f1e679e07576bd517d2296477666b86f4388be30030b8127a2ce5ef
|
7
|
+
data.tar.gz: afd22e1654d2ca4a66f648e96d08cfd9f52ba3c780d19d4352344214dd6b753ba85236f8e2b0bbf47ecce2c1bd272ac6c33ed6592c0a96838db3c0ea50b327c5
|
data/.rubocop.yml
CHANGED
@@ -1,13 +1,5 @@
|
|
1
1
|
AllCops:
|
2
|
-
TargetRubyVersion: 3.
|
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
|
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
|
8
|
+
gem 'rake', '~> 13.0'
|
9
9
|
|
10
|
-
gem
|
10
|
+
gem 'rubocop', '~> 1.21'
|
data/Gemfile.lock
CHANGED
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
|
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
|
-
|
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
|
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
data/boardgame_engine.gemspec
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative 'lib/boardgame_engine/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name =
|
6
|
+
spec.name = 'boardgame_engine'
|
7
7
|
spec.version = BoardgameEngine::VERSION
|
8
|
-
spec.authors = [
|
9
|
-
spec.email = [
|
8
|
+
spec.authors = ['FortHoney']
|
9
|
+
spec.email = ['castlehoneyjung@gmail.com']
|
10
10
|
|
11
|
-
spec.summary =
|
12
|
-
spec.homepage =
|
13
|
-
spec.required_ruby_version =
|
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[
|
18
|
-
spec.metadata[
|
19
|
-
spec.metadata[
|
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 =
|
28
|
+
spec.bindir = 'exe'
|
29
29
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
-
spec.require_paths = [
|
30
|
+
spec.require_paths = ['lib']
|
31
31
|
|
32
|
-
spec.add_development_dependency
|
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
|
66
|
-
|
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
|
-
|
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.
|
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
|
63
|
+
when 'y'
|
99
64
|
tutorial
|
100
|
-
when
|
101
|
-
puts
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
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
|
-
|
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
|