boardgame_engine 0.1.0 → 0.1.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/lib/boardgame_engine/boardgame.rb +111 -109
- data/lib/boardgame_engine/chess.rb +268 -0
- data/lib/boardgame_engine/connect4.rb +77 -0
- data/lib/boardgame_engine/sample_games.rb +5 -0
- data/lib/boardgame_engine/version.rb +1 -1
- data/lib/boardgame_engine.rb +3 -4
- metadata +5 -5
- data/lib/boardgame_engine/chess/chess.rb +0 -103
- data/lib/boardgame_engine/chess/chess_pieces.rb +0 -164
- data/lib/boardgame_engine/connect4/connect4.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7129074317f7ea44e65ad18e5952e2401e47be99186e26dd42b0c083ebc91797
|
4
|
+
data.tar.gz: f87aebcb6502cbfd9d8ee04e60b775f1497e09527863f3f820a1edb202297330
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c1ab01918757d936b1ee97cf8d75bbf19a300e71450b0c1896fcd9cdda0f804b25324b71ef01929d8d231f96ef7af15ec023690dc52d5a738e8b363da43c4f8
|
7
|
+
data.tar.gz: b10198cde214fe10e7a861a27419e6235b25a84ef1fac0992de66e0297668da8e3f600effaae0049c12a5343e761dccfb623594bc2621d40b24063e8b8d82cb0
|
@@ -3,142 +3,144 @@
|
|
3
3
|
# All classes in this script are intended to be abstract, meaning they should
|
4
4
|
# not be called on their own.
|
5
5
|
|
6
|
-
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def display(show_row: false, show_col: false)
|
18
|
-
if show_row
|
19
|
-
@board.each_with_index { |row, idx| puts("#{idx} " + format_row(row)) }
|
20
|
-
else
|
21
|
-
@board.each { |row| puts format_row(row) }
|
6
|
+
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 } }
|
22
16
|
end
|
23
|
-
return unless show_col
|
24
17
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
36
37
|
|
37
|
-
|
38
|
-
|
38
|
+
destination
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
+
private
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
def format_row(row)
|
44
|
+
row.map { |elem| "[#{elem.nil? ? " " : elem}]" }.join
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
47
|
+
def spot_playable?(piece, row, col)
|
48
|
+
piece.possible_moves.include? [row, col]
|
49
|
+
end
|
48
50
|
end
|
49
|
-
end
|
50
51
|
|
51
|
-
# Class representing a player in a game
|
52
|
-
class Player
|
53
|
-
|
52
|
+
# Class representing a player in a game
|
53
|
+
class Player
|
54
|
+
attr_reader :name
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
def initialize(name)
|
57
|
+
@name = name
|
58
|
+
end
|
58
59
|
|
59
|
-
|
60
|
-
|
60
|
+
def to_s
|
61
|
+
@name.to_s
|
62
|
+
end
|
61
63
|
end
|
62
|
-
end
|
63
64
|
|
64
|
-
# Class for running a board game.
|
65
|
-
class Boardgame
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
65
|
+
# Class for running a board game.
|
66
|
+
class Boardgame
|
67
|
+
EXIT_INSTRUCTIONS ||= "Try a sample input or input 'back' to leave the " \
|
68
|
+
"tutorial. Type in 'exit' anytime to exit the game fully"
|
69
|
+
|
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
|
76
77
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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)
|
84
|
+
|
85
|
+
puts "Welcome to #{@game}!"
|
86
|
+
@game.onboarding if do_onboarding
|
87
|
+
puts "Starting #{@game}..."
|
88
|
+
@game.start
|
89
|
+
end
|
89
90
|
|
90
|
-
|
91
|
-
|
92
|
-
|
91
|
+
def to_s(game_name = "boardgame")
|
92
|
+
"#{game_name} between #{@player1} and #{@player2}"
|
93
|
+
end
|
93
94
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
95
|
+
def onboarding
|
96
|
+
puts "Would you like a tutorial on how to play on this program? \n(y, n)"
|
97
|
+
case gets.chomp
|
98
|
+
when "y"
|
99
|
+
tutorial
|
100
|
+
when "n"
|
101
|
+
puts "Skipping tutorial"
|
102
|
+
else
|
103
|
+
puts 'Please answer either "y" or "n"'
|
104
|
+
onboarding
|
105
|
+
end
|
104
106
|
end
|
105
|
-
end
|
106
107
|
|
107
|
-
|
108
|
-
|
109
|
-
input = gets.chomp
|
110
|
-
until input == "back"
|
111
|
-
exit if input == "exit"
|
112
|
-
puts valid_input?(input) ? "Valid input!" : "Invalid input"
|
108
|
+
def tutorial
|
109
|
+
puts @instructions + Boardgame::EXIT_INSTRUCTIONS
|
113
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
|
114
116
|
end
|
115
|
-
end
|
116
117
|
|
117
|
-
|
118
|
-
|
119
|
-
@board.display
|
120
|
-
until @winner
|
121
|
-
play_turn
|
118
|
+
def start(turn = @player1)
|
119
|
+
@turn = turn
|
122
120
|
@board.display
|
121
|
+
until @winner
|
122
|
+
play_turn
|
123
|
+
@board.display
|
124
|
+
end
|
125
|
+
puts "#{@winner} wins!"
|
123
126
|
end
|
124
|
-
puts "#{@winner} wins!"
|
125
|
-
end
|
126
127
|
|
127
|
-
|
128
|
+
protected
|
128
129
|
|
129
|
-
|
130
|
-
input = gets.chomp
|
131
|
-
until valid_input?(input)
|
132
|
-
exit if input == "exit"
|
133
|
-
return input if special_commands.include?(input)
|
134
|
-
|
135
|
-
puts "Input is in the wrong format or out of bounds. Try again"
|
130
|
+
def proper_format_input(special_commands = [])
|
136
131
|
input = gets.chomp
|
132
|
+
until valid_input?(input)
|
133
|
+
exit if input == "exit"
|
134
|
+
return input if special_commands.include?(input)
|
135
|
+
|
136
|
+
puts "Input is in the wrong format or out of bounds. Try again"
|
137
|
+
input = gets.chomp
|
138
|
+
end
|
139
|
+
input
|
137
140
|
end
|
138
|
-
input
|
139
|
-
end
|
140
141
|
|
141
|
-
|
142
|
-
|
142
|
+
def setup_board(board)
|
143
|
+
board.new
|
144
|
+
end
|
143
145
|
end
|
144
146
|
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "boardgame"
|
4
|
+
require_relative "multiplayergame"
|
5
|
+
|
6
|
+
module SampleChess
|
7
|
+
class ChessBoard < BoardgameEngine::Board
|
8
|
+
attr_reader :board
|
9
|
+
|
10
|
+
def initialize(player1, player2)
|
11
|
+
super(8, 8)
|
12
|
+
setup(player1, player2)
|
13
|
+
end
|
14
|
+
|
15
|
+
def display
|
16
|
+
super(show_row: true, show_col: true)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def setup(player1, player2)
|
22
|
+
set_pawns(player1, player2)
|
23
|
+
set_non_pawns(player1, player2)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_pawns(player1, player2)
|
27
|
+
@board[1] = @board[1].map { Pawn.new(player1, 1) }
|
28
|
+
@board[-2] = @board[-2].map { Pawn.new(player2, -1) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_non_pawns(player1, player2)
|
32
|
+
pieces = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
|
33
|
+
pieces.each_with_index do |piece, idx|
|
34
|
+
@board[0][idx] = piece.new(player1)
|
35
|
+
@board[7][7 - idx] = piece.new(player2)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Chess < BoardgameEngine::Boardgame
|
41
|
+
include TwoPlayers
|
42
|
+
|
43
|
+
def initialize(name1 = "Player 1", name2 = "Player 2")
|
44
|
+
@instructions = "You can select spots on the board by inputting the row " \
|
45
|
+
"and column with a comma in between. See example below\n1, 1\n"
|
46
|
+
super(ChessBoard, @instructions, name1, name2)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
super("chess")
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def valid_input?(input)
|
56
|
+
coords = input.split(",")
|
57
|
+
coords.all? { |c| c.match?(/[[:digit:]]/) && c.to_i.between?(0, 7) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def valid_piece?(piece)
|
61
|
+
!piece.nil? && piece.owner == @turn
|
62
|
+
end
|
63
|
+
|
64
|
+
def select_piece
|
65
|
+
input = proper_format_input
|
66
|
+
input.split(",").map(&:to_i) => [row, col]
|
67
|
+
if valid_piece? @board.board.dig(row, col)
|
68
|
+
[row, col]
|
69
|
+
else
|
70
|
+
puts "Invalid piece. Try again"
|
71
|
+
select_piece
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def select_destination(piece, row, col)
|
76
|
+
input = proper_format_input(["back"])
|
77
|
+
return "back" if input == "back"
|
78
|
+
|
79
|
+
input.split(",").map(&:to_i) => [end_row, end_col]
|
80
|
+
if piece.valid_move?(row, col, end_row, end_col, @board.board)
|
81
|
+
[end_row, end_col]
|
82
|
+
else
|
83
|
+
puts "Invalid destination. Try again"
|
84
|
+
select_destination(piece, row, col)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def play_turn
|
89
|
+
puts "#{@turn}'s turn\nSelect your piece"
|
90
|
+
select_piece => [row, col]
|
91
|
+
piece = @board.board[row][col]
|
92
|
+
puts "Select where to move #{piece} to. Type back to reselect piece"
|
93
|
+
dest = select_destination(piece, row, col)
|
94
|
+
return if dest == "back"
|
95
|
+
|
96
|
+
killed = @board.move_piece(row, col, dest[0], dest[1])
|
97
|
+
@winner = piece.owner if killed.is_a?(King)
|
98
|
+
change_turn
|
99
|
+
end
|
100
|
+
|
101
|
+
def setup_board(board)
|
102
|
+
board.new(@player1, @player2)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class ChessPiece
|
107
|
+
attr_accessor :status
|
108
|
+
attr_reader :owner
|
109
|
+
|
110
|
+
def initialize(owner, name)
|
111
|
+
@kill_log = []
|
112
|
+
@status = "alive"
|
113
|
+
@owner = owner
|
114
|
+
@name = name
|
115
|
+
end
|
116
|
+
|
117
|
+
def kill(other)
|
118
|
+
@kill_log.push(other)
|
119
|
+
other.status = "dead"
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_s
|
123
|
+
@name.to_s
|
124
|
+
end
|
125
|
+
|
126
|
+
protected
|
127
|
+
|
128
|
+
def valid_diag_move?(row, col, end_row, end_col, board)
|
129
|
+
((end_row - row).abs == (end_col - col).abs) \
|
130
|
+
&& clear_path?(row, col, end_row, end_col, board)
|
131
|
+
end
|
132
|
+
|
133
|
+
def valid_horz_move?(row, col, end_row, end_col, board)
|
134
|
+
(end_row == row) && clear_path?(row, col, end_row, end_col, board)
|
135
|
+
end
|
136
|
+
|
137
|
+
def valid_vert_move?(row, col, end_row, end_col, board)
|
138
|
+
(end_col == col) && clear_path?(row, col, end_row, end_col, board)
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def next_cell(row, col, end_row, end_col)
|
144
|
+
row_move = 0
|
145
|
+
col_move = 0
|
146
|
+
|
147
|
+
col_move = (end_col - col) / (end_col - col).abs if end_col != col
|
148
|
+
row_move = (end_row - row) / (end_row - row).abs if end_row != row
|
149
|
+
|
150
|
+
[row + row_move, col + col_move]
|
151
|
+
end
|
152
|
+
|
153
|
+
def clear_path?(row, col, end_row, end_col, board)
|
154
|
+
current_tile = board.dig(row, col)
|
155
|
+
if (row == end_row) && (col == end_col)
|
156
|
+
current_tile.nil? || (current_tile.owner != @owner)
|
157
|
+
elsif current_tile.nil? || current_tile.equal?(self)
|
158
|
+
next_cell(row, col, end_row, end_col) => [next_row, next_col]
|
159
|
+
clear_path?(next_row, next_col, end_row, end_col, board)
|
160
|
+
else
|
161
|
+
false
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class Pawn < ChessPiece
|
167
|
+
def initialize(owner, front)
|
168
|
+
super(owner, "p")
|
169
|
+
@first_move = true
|
170
|
+
@front = front
|
171
|
+
end
|
172
|
+
|
173
|
+
def valid_move?(row, col, end_row, end_col, board)
|
174
|
+
return false unless valid_forward_move?(row, end_row)
|
175
|
+
|
176
|
+
if col == end_col # only forward
|
177
|
+
valid_dest = board.dig(end_row, end_col).nil?
|
178
|
+
@first_move = false if valid_dest
|
179
|
+
return valid_dest
|
180
|
+
elsif (col - end_col).abs == 1 # diagonal movement
|
181
|
+
other_piece = board.dig(end_row, end_col)
|
182
|
+
return other_piece && (other_piece.owner != @owner)
|
183
|
+
end
|
184
|
+
false
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def valid_forward_move?(row, end_row)
|
190
|
+
if @first_move
|
191
|
+
(row + @front * 2 == end_row) || (row + @front == end_row)
|
192
|
+
else
|
193
|
+
row + @front == end_row
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class Queen < ChessPiece
|
199
|
+
def initialize(owner)
|
200
|
+
super(owner, "Q")
|
201
|
+
end
|
202
|
+
|
203
|
+
def valid_move?(row, col, end_row, end_col, board)
|
204
|
+
valid_diag_move?(row, col, end_row, end_col, board) \
|
205
|
+
|| valid_horz_move?(row, col, end_row, end_col, board) \
|
206
|
+
|| valid_vert_move?(row, col, end_row, end_col, board)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
class Rook < ChessPiece
|
211
|
+
def initialize(owner)
|
212
|
+
super(owner, "R")
|
213
|
+
end
|
214
|
+
|
215
|
+
def valid_move?(row, col, end_row, end_col, board)
|
216
|
+
valid_horz_move?(row, col, end_row, end_col, board) \
|
217
|
+
|| valid_vert_move?(row, col, end_row, end_col, board)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
class Bishop < ChessPiece
|
222
|
+
def initialize(owner)
|
223
|
+
super(owner, "B")
|
224
|
+
end
|
225
|
+
|
226
|
+
def valid_move?(row, col, end_row, end_col, board)
|
227
|
+
valid_diag_move?(row, col, end_row, end_col, board)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class King < ChessPiece
|
232
|
+
def initialize(owner)
|
233
|
+
super(owner, "K")
|
234
|
+
end
|
235
|
+
|
236
|
+
def valid_move?(row, col, end_row, end_col, board)
|
237
|
+
return false unless (row - end_row).abs == 1 && (col - end_col).abs == 1
|
238
|
+
|
239
|
+
valid_diag_move?(row, col, end_row, end_col, board) \
|
240
|
+
|| valid_horz_move?(row, col, end_row, end_col, board) \
|
241
|
+
|| valid_vert_move?(row, col, end_row, end_col, board)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
class Knight < ChessPiece
|
246
|
+
def initialize(owner)
|
247
|
+
# K was already taken by king, so I had to choose N
|
248
|
+
super(owner, "N")
|
249
|
+
end
|
250
|
+
|
251
|
+
def valid_move?(row, col, end_row, end_col, board)
|
252
|
+
within_movement(row, col, end_row, end_col) \
|
253
|
+
&& not_occupied(end_row, end_col, board)
|
254
|
+
end
|
255
|
+
|
256
|
+
private
|
257
|
+
|
258
|
+
def within_movement(row, col, end_row, end_col)
|
259
|
+
((row - end_row).abs == 2 and (col - end_col).abs == 1) \
|
260
|
+
|| ((row - end_row).abs == 1 and (col - end_col).abs == 2)
|
261
|
+
end
|
262
|
+
|
263
|
+
def not_occupied(end_row, end_col, board)
|
264
|
+
spot = board.dig(end_row, end_col)
|
265
|
+
spot.nil? || spot.owner != @owner
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "boardgame"
|
4
|
+
require_relative "multiplayergame"
|
5
|
+
|
6
|
+
module SampleConnect4
|
7
|
+
class Connect4Board < BoardgameEngine::Board
|
8
|
+
def initialize
|
9
|
+
super(6, 7)
|
10
|
+
end
|
11
|
+
|
12
|
+
def display
|
13
|
+
super(show_col: true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def drop_chip(col, owner)
|
17
|
+
@board.reverse_each do |row|
|
18
|
+
if row[col].nil?
|
19
|
+
row[col] = owner
|
20
|
+
break
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Connect4 < BoardgameEngine::Boardgame
|
27
|
+
include TwoPlayers
|
28
|
+
|
29
|
+
@instructions = "You can select which column to drop you chip into by" \
|
30
|
+
" typing in the row number."
|
31
|
+
|
32
|
+
def initialize(name1 = "Player 1", name2 = "Player 2")
|
33
|
+
super(Connect4Board, @instructions, name1, name2)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
super("connect-four")
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def valid_input?(input)
|
43
|
+
input.match?(/[[:digit:]]/) && input.to_i.between?(0, 6)
|
44
|
+
end
|
45
|
+
|
46
|
+
def play_turn
|
47
|
+
puts "#{@turn}'s turn. Choose a column to drop your chip in"
|
48
|
+
col = proper_format_input.to_i
|
49
|
+
@board.drop_chip(col, @turn)
|
50
|
+
@winner = @turn if win?
|
51
|
+
change_turn
|
52
|
+
end
|
53
|
+
|
54
|
+
def win?
|
55
|
+
[@board.board,
|
56
|
+
@board.board.transpose,
|
57
|
+
align_diagonally(@board.board),
|
58
|
+
align_diagonally(@board.board.transpose)].each do |config|
|
59
|
+
config.each { |direction| return true if four_in_a_row? direction }
|
60
|
+
end
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
def four_in_a_row?(row)
|
65
|
+
counts = row.chunk { |x| x }.map { |x, xs| [x, xs.length] }
|
66
|
+
return true if counts.any? { |x, count| count > 3 && !x.nil? }
|
67
|
+
end
|
68
|
+
|
69
|
+
def align_diagonally(board)
|
70
|
+
board.map.with_index do |row, idx|
|
71
|
+
left_filler = Array.new(board.length - 1 - idx, nil)
|
72
|
+
right_filler = Array.new(idx, nil)
|
73
|
+
left_filler + row + right_filler
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/boardgame_engine.rb
CHANGED
@@ -2,10 +2,9 @@
|
|
2
2
|
|
3
3
|
require_relative "boardgame_engine/version"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
require_relative "boardgame_engine/connect4/connect4"
|
5
|
+
require "boardgame_engine/boardgame"
|
6
|
+
require "boardgame_engine/multiplayergame"
|
7
|
+
require "boardgame_engine/sample_games"
|
9
8
|
|
10
9
|
module BoardgameEngine
|
11
10
|
class Error < StandardError; end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: boardgame_engine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- FortHoney
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-10-
|
11
|
+
date: 2022-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -40,10 +40,10 @@ files:
|
|
40
40
|
- boardgame_engine.gemspec
|
41
41
|
- lib/boardgame_engine.rb
|
42
42
|
- lib/boardgame_engine/boardgame.rb
|
43
|
-
- lib/boardgame_engine/chess
|
44
|
-
- lib/boardgame_engine/
|
45
|
-
- lib/boardgame_engine/connect4/connect4.rb
|
43
|
+
- lib/boardgame_engine/chess.rb
|
44
|
+
- lib/boardgame_engine/connect4.rb
|
46
45
|
- lib/boardgame_engine/multiplayergame.rb
|
46
|
+
- lib/boardgame_engine/sample_games.rb
|
47
47
|
- lib/boardgame_engine/version.rb
|
48
48
|
- sig/boardgame_engine.rbs
|
49
49
|
homepage: https://github.com/Forthoney/boardgame_engine
|
@@ -1,103 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "./lib/boardgame_engine"
|
4
|
-
require "./lib/boardgame_engine/chess/chess_pieces"
|
5
|
-
|
6
|
-
class ChessBoard < Board
|
7
|
-
attr_reader :board
|
8
|
-
|
9
|
-
def initialize(player1, player2)
|
10
|
-
super(8, 8)
|
11
|
-
setup(player1, player2)
|
12
|
-
end
|
13
|
-
|
14
|
-
def display
|
15
|
-
super(show_row: true, show_col: true)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def setup(player1, player2)
|
21
|
-
set_pawns(player1, player2)
|
22
|
-
set_non_pawns(player1, player2)
|
23
|
-
end
|
24
|
-
|
25
|
-
def set_pawns(player1, player2)
|
26
|
-
@board[1] = @board[1].map { Pawn.new(player1, 1) }
|
27
|
-
@board[-2] = @board[-2].map { Pawn.new(player2, -1) }
|
28
|
-
end
|
29
|
-
|
30
|
-
def set_non_pawns(player1, player2)
|
31
|
-
pieces = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
|
32
|
-
pieces.each_with_index do |piece, idx|
|
33
|
-
@board[0][idx] = piece.new(player1)
|
34
|
-
@board[7][7 - idx] = piece.new(player2)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
class Chess < Boardgame
|
40
|
-
include TwoPlayers
|
41
|
-
|
42
|
-
def initialize(name1 = "Player 1", name2 = "Player 2")
|
43
|
-
@instructions = "You can select spots on the board by inputting the row " \
|
44
|
-
"and column with a comma in between. See example below\n1, 1\n"
|
45
|
-
super(ChessBoard, @instructions, name1, name2)
|
46
|
-
end
|
47
|
-
|
48
|
-
def to_s
|
49
|
-
super("chess")
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def valid_input?(input)
|
55
|
-
coords = input.split(",")
|
56
|
-
coords.all? { |c| c.match?(/[[:digit:]]/) && c.to_i.between?(0, 7) }
|
57
|
-
end
|
58
|
-
|
59
|
-
def valid_piece?(piece)
|
60
|
-
!piece.nil? && piece.owner == @turn
|
61
|
-
end
|
62
|
-
|
63
|
-
def select_piece
|
64
|
-
input = proper_format_input
|
65
|
-
input.split(",").map(&:to_i) => [row, col]
|
66
|
-
if valid_piece? @board.board.dig(row, col)
|
67
|
-
[row, col]
|
68
|
-
else
|
69
|
-
puts "Invalid piece. Try again"
|
70
|
-
select_piece
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def select_destination(piece, row, col)
|
75
|
-
input = proper_format_input(["back"])
|
76
|
-
return "back" if input == "back"
|
77
|
-
|
78
|
-
input.split(",").map(&:to_i) => [end_row, end_col]
|
79
|
-
if piece.valid_move?(row, col, end_row, end_col, @board.board)
|
80
|
-
[end_row, end_col]
|
81
|
-
else
|
82
|
-
puts "Invalid destination. Try again"
|
83
|
-
select_destination(piece, row, col)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def play_turn
|
88
|
-
puts "#{@turn}'s turn\nSelect your piece"
|
89
|
-
select_piece => [row, col]
|
90
|
-
piece = @board.board[row][col]
|
91
|
-
puts "Select where to move #{piece} to. Type back to reselect piece"
|
92
|
-
dest = select_destination(piece, row, col)
|
93
|
-
return if dest == "back"
|
94
|
-
|
95
|
-
killed = @board.move_piece(row, col, dest[0], dest[1])
|
96
|
-
@winner = piece.owner if killed.is_a?(King)
|
97
|
-
change_turn
|
98
|
-
end
|
99
|
-
|
100
|
-
def setup_board(board)
|
101
|
-
board.new(@player1, @player2)
|
102
|
-
end
|
103
|
-
end
|
@@ -1,164 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class ChessPiece
|
4
|
-
attr_accessor :status
|
5
|
-
attr_reader :owner
|
6
|
-
|
7
|
-
def initialize(owner, name)
|
8
|
-
@kill_log = []
|
9
|
-
@status = "alive"
|
10
|
-
@owner = owner
|
11
|
-
@name = name
|
12
|
-
end
|
13
|
-
|
14
|
-
def kill(other)
|
15
|
-
@kill_log.push(other)
|
16
|
-
other.status = "dead"
|
17
|
-
end
|
18
|
-
|
19
|
-
def to_s
|
20
|
-
@name.to_s
|
21
|
-
end
|
22
|
-
|
23
|
-
protected
|
24
|
-
|
25
|
-
def valid_diag_move?(row, col, end_row, end_col, board)
|
26
|
-
((end_row - row).abs == (end_col - col).abs) \
|
27
|
-
&& clear_path?(row, col, end_row, end_col, board)
|
28
|
-
end
|
29
|
-
|
30
|
-
def valid_horz_move?(row, col, end_row, end_col, board)
|
31
|
-
(end_row == row) && clear_path?(row, col, end_row, end_col, board)
|
32
|
-
end
|
33
|
-
|
34
|
-
def valid_vert_move?(row, col, end_row, end_col, board)
|
35
|
-
(end_col == col) && clear_path?(row, col, end_row, end_col, board)
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def next_cell(row, col, end_row, end_col)
|
41
|
-
row_move = 0
|
42
|
-
col_move = 0
|
43
|
-
|
44
|
-
col_move = (end_col - col) / (end_col - col).abs if end_col != col
|
45
|
-
row_move = (end_row - row) / (end_row - row).abs if end_row != row
|
46
|
-
|
47
|
-
[row + row_move, col + col_move]
|
48
|
-
end
|
49
|
-
|
50
|
-
def clear_path?(row, col, end_row, end_col, board)
|
51
|
-
current_tile = board.dig(row, col)
|
52
|
-
if (row == end_row) && (col == end_col)
|
53
|
-
current_tile.nil? || (current_tile.owner != @owner)
|
54
|
-
elsif current_tile.nil? || current_tile.equal?(self)
|
55
|
-
next_cell(row, col, end_row, end_col) => [next_row, next_col]
|
56
|
-
clear_path?(next_row, next_col, end_row, end_col, board)
|
57
|
-
else
|
58
|
-
false
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
class Pawn < ChessPiece
|
64
|
-
def initialize(owner, front)
|
65
|
-
super(owner, "p")
|
66
|
-
@first_move = true
|
67
|
-
@front = front
|
68
|
-
end
|
69
|
-
|
70
|
-
def valid_move?(row, col, end_row, end_col, board)
|
71
|
-
return false unless valid_forward_move?(row, end_row)
|
72
|
-
|
73
|
-
if col == end_col # only forward
|
74
|
-
valid_dest = board.dig(end_row, end_col).nil?
|
75
|
-
@first_move = false if valid_dest
|
76
|
-
return valid_dest
|
77
|
-
elsif (col - end_col).abs == 1 # diagonal movement
|
78
|
-
other_piece = board.dig(end_row, end_col)
|
79
|
-
return other_piece && (other_piece.owner != @owner)
|
80
|
-
end
|
81
|
-
false
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def valid_forward_move?(row, end_row)
|
87
|
-
if @first_move
|
88
|
-
(row + @front * 2 == end_row) || (row + @front == end_row)
|
89
|
-
else
|
90
|
-
row + @front == end_row
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
class Queen < ChessPiece
|
96
|
-
def initialize(owner)
|
97
|
-
super(owner, "Q")
|
98
|
-
end
|
99
|
-
|
100
|
-
def valid_move?(row, col, end_row, end_col, board)
|
101
|
-
valid_diag_move?(row, col, end_row, end_col, board) \
|
102
|
-
|| valid_horz_move?(row, col, end_row, end_col, board) \
|
103
|
-
|| valid_vert_move?(row, col, end_row, end_col, board)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
class Rook < ChessPiece
|
108
|
-
def initialize(owner)
|
109
|
-
super(owner, "R")
|
110
|
-
end
|
111
|
-
|
112
|
-
def valid_move?(row, col, end_row, end_col, board)
|
113
|
-
valid_horz_move?(row, col, end_row, end_col, board) \
|
114
|
-
|| valid_vert_move?(row, col, end_row, end_col, board)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
class Bishop < ChessPiece
|
119
|
-
def initialize(owner)
|
120
|
-
super(owner, "B")
|
121
|
-
end
|
122
|
-
|
123
|
-
def valid_move?(row, col, end_row, end_col, board)
|
124
|
-
valid_diag_move?(row, col, end_row, end_col, board)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
class King < ChessPiece
|
129
|
-
def initialize(owner)
|
130
|
-
super(owner, "K")
|
131
|
-
end
|
132
|
-
|
133
|
-
def valid_move?(row, col, end_row, end_col, board)
|
134
|
-
return false unless (row - end_row).abs == 1 && (col - end_col).abs == 1
|
135
|
-
|
136
|
-
valid_diag_move?(row, col, end_row, end_col, board) \
|
137
|
-
|| valid_horz_move?(row, col, end_row, end_col, board) \
|
138
|
-
|| valid_vert_move?(row, col, end_row, end_col, board)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
class Knight < ChessPiece
|
143
|
-
def initialize(owner)
|
144
|
-
# K was already taken by king, so I had to choose N
|
145
|
-
super(owner, "N")
|
146
|
-
end
|
147
|
-
|
148
|
-
def valid_move?(row, col, end_row, end_col, board)
|
149
|
-
within_movement(row, col, end_row, end_col) \
|
150
|
-
&& not_occupied(end_row, end_col, board)
|
151
|
-
end
|
152
|
-
|
153
|
-
private
|
154
|
-
|
155
|
-
def within_movement(row, col, end_row, end_col)
|
156
|
-
((row - end_row).abs == 2 and (col - end_col).abs == 1) \
|
157
|
-
|| ((row - end_row).abs == 1 and (col - end_col).abs == 2)
|
158
|
-
end
|
159
|
-
|
160
|
-
def not_occupied(end_row, end_col, board)
|
161
|
-
spot = board.dig(end_row, end_col)
|
162
|
-
spot.nil? || spot.owner != @owner
|
163
|
-
end
|
164
|
-
end
|
@@ -1,74 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "./lib/boardgame_engine"
|
4
|
-
|
5
|
-
class Connect4Board < Board
|
6
|
-
def initialize
|
7
|
-
super(6, 7)
|
8
|
-
end
|
9
|
-
|
10
|
-
def display
|
11
|
-
super(show_col: true)
|
12
|
-
end
|
13
|
-
|
14
|
-
def drop_chip(col, owner)
|
15
|
-
@board.reverse_each do |row|
|
16
|
-
if row[col].nil?
|
17
|
-
row[col] = owner
|
18
|
-
break
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
class Connect4 < Boardgame
|
25
|
-
include TwoPlayers
|
26
|
-
|
27
|
-
@instructions = "You can select which column to drop you chip into by" \
|
28
|
-
" typing in the row number."
|
29
|
-
|
30
|
-
def initialize(name1 = "Player 1", name2 = "Player 2")
|
31
|
-
super(Connect4Board, @instructions, name1, name2)
|
32
|
-
end
|
33
|
-
|
34
|
-
def to_s
|
35
|
-
super("connect-four")
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def valid_input?(input)
|
41
|
-
input.match?(/[[:digit:]]/) && input.to_i.between?(0, 6)
|
42
|
-
end
|
43
|
-
|
44
|
-
def play_turn
|
45
|
-
puts "#{@turn}'s turn. Choose a column to drop your chip in"
|
46
|
-
col = proper_format_input.to_i
|
47
|
-
@board.drop_chip(col, @turn)
|
48
|
-
@winner = @turn if win?
|
49
|
-
change_turn
|
50
|
-
end
|
51
|
-
|
52
|
-
def win?
|
53
|
-
[@board.board,
|
54
|
-
@board.board.transpose,
|
55
|
-
align_diagonally(@board.board),
|
56
|
-
align_diagonally(@board.board.transpose)].each do |config|
|
57
|
-
config.each { |direction| return true if four_in_a_row? direction }
|
58
|
-
end
|
59
|
-
false
|
60
|
-
end
|
61
|
-
|
62
|
-
def four_in_a_row?(row)
|
63
|
-
counts = row.chunk { |x| x }.map { |x, xs| [x, xs.length] }
|
64
|
-
return true if counts.any? { |x, count| count > 3 && !x.nil? }
|
65
|
-
end
|
66
|
-
|
67
|
-
def align_diagonally(board)
|
68
|
-
board.map.with_index do |row, idx|
|
69
|
-
left_filler = Array.new(board.length - 1 - idx, nil)
|
70
|
-
right_filler = Array.new(idx, nil)
|
71
|
-
left_filler + row + right_filler
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|