boardgame_engine 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|