boardgame_engine 0.1.1 → 0.2.0
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/Rakefile +2 -2
- data/boardgame_engine.gemspec +13 -13
- data/lib/boardgame_engine/board_modules.rb +105 -0
- data/lib/boardgame_engine/boardgame.rb +87 -55
- data/lib/boardgame_engine/chess.rb +108 -143
- data/lib/boardgame_engine/connect4.rb +19 -39
- 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: c8a00224a55f64e290296e59c90686e9984141c43b3ce1d588cbe2a6f382d906
|
4
|
+
data.tar.gz: 83ec3bd763ae34688b1f99f8e2c54d9bc9c0229430fc80ae5211db81a9ce47a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3d1d0647a5ff2b85b619aa85f72dc6a1f3638074a2333b22fed2535239c0feb64c43e4ac97eb98bbff45c77ebf513bd9d401299b8046483a2520dca005fdf1f
|
7
|
+
data.tar.gz: eb8768b54c655792cf5f5270ddcc30867b1afd4e80d8382e97704873742b1dd8964925ed1683eff327b386c0d0f9d2aebf454ff1ecfe4cd1bed326dfd1cb3c41
|
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/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,105 @@
|
|
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
|
+
# Make a 2D Array representing the board
|
8
|
+
#
|
9
|
+
# @param [Integer] row the number of rows
|
10
|
+
# @param [Integer] col the number of columns
|
11
|
+
#
|
12
|
+
# @return [Array<Array>] A 2D Array
|
13
|
+
def generate_board(row, col)
|
14
|
+
Array.new(row) { Array.new(col) { nil } }
|
15
|
+
end
|
16
|
+
|
17
|
+
# Print a visual representation of the
|
18
|
+
#
|
19
|
+
# @param [Boolean] show_row whether to show row labels
|
20
|
+
# @param [Boolean] show_col whether to show column labels
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
def display(show_row: false, show_col: false)
|
24
|
+
if show_row
|
25
|
+
@board.each_with_index { |row, idx| puts("#{idx} " + format_row(row)) }
|
26
|
+
else
|
27
|
+
@board.each { |row| puts format_row(row) }
|
28
|
+
end
|
29
|
+
|
30
|
+
return unless show_col
|
31
|
+
|
32
|
+
column_spacer = show_row ? ' ' : ''
|
33
|
+
puts format_col(column_spacer)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_piece_at(location)
|
37
|
+
row, col = location
|
38
|
+
@board.dig(row, col)
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_piece_at(location, piece)
|
42
|
+
row, col = location
|
43
|
+
@board[row][col] = piece
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_board_input?(input, only_row: false, only_col: false)
|
47
|
+
if only_row || only_col
|
48
|
+
input.match?(/[0-#{@board.length - 1}]/)
|
49
|
+
else
|
50
|
+
input.match?(/[0-#{@board.length - 1}], [0-#{@board[0].length - 1}]$/)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_input(input)
|
55
|
+
input.split(',').map(&:to_i)
|
56
|
+
end
|
57
|
+
|
58
|
+
def consecutive?(row: true, col: true, diagonal: true)
|
59
|
+
configs = []
|
60
|
+
configs << @board if row
|
61
|
+
configs << @board.transpose if col
|
62
|
+
configs << align_diagonal(@board) << align_diagonal(@board.transpose) if diagonal
|
63
|
+
|
64
|
+
configs.each do |board|
|
65
|
+
board.each { |array| return true if row_consecutive?(4, array) }
|
66
|
+
end
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def row_consecutive?(num, array)
|
73
|
+
elem_counts = array.chunk { |x| x }.map { |x, xs| [x, xs.length] }
|
74
|
+
elem_counts.any? { |x, count| count >= num && x }
|
75
|
+
end
|
76
|
+
|
77
|
+
def align_diagonal(board)
|
78
|
+
board.map.with_index do |row, idx|
|
79
|
+
left_filler = Array.new(board.length - 1 - idx, nil)
|
80
|
+
right_filler = Array.new(idx, nil)
|
81
|
+
left_filler + row + right_filler
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_row(row)
|
86
|
+
row.map { |elem| "[#{elem.nil? ? ' ' : elem}]" }.join
|
87
|
+
end
|
88
|
+
|
89
|
+
def format_col(column_spacer)
|
90
|
+
@board[0].each_index.reduce(column_spacer) { |str, idx| str + " #{idx} " }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Boards with movable pieces
|
95
|
+
module MovablePiece
|
96
|
+
def move_piece(start_location, end_location, set_start_to: nil)
|
97
|
+
piece = get_piece_at(start_location)
|
98
|
+
set_piece_at(start_location, set_start_to)
|
99
|
+
destination_piece = get_piece_at(end_location)
|
100
|
+
set_piece_at(end_location, piece)
|
101
|
+
|
102
|
+
destination_piece
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -11,39 +11,8 @@ module BoardgameEngine
|
|
11
11
|
class Board
|
12
12
|
attr_reader :board
|
13
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
14
|
private
|
42
15
|
|
43
|
-
def format_row(row)
|
44
|
-
row.map { |elem| "[#{elem.nil? ? " " : elem}]" }.join
|
45
|
-
end
|
46
|
-
|
47
16
|
def spot_playable?(piece, row, col)
|
48
17
|
piece.possible_moves.include? [row, col]
|
49
18
|
end
|
@@ -53,6 +22,8 @@ module BoardgameEngine
|
|
53
22
|
class Player
|
54
23
|
attr_reader :name
|
55
24
|
|
25
|
+
# Creates a player object with the given name
|
26
|
+
# @param [String] name
|
56
27
|
def initialize(name)
|
57
28
|
@name = name
|
58
29
|
end
|
@@ -62,25 +33,26 @@ module BoardgameEngine
|
|
62
33
|
end
|
63
34
|
end
|
64
35
|
|
65
|
-
# Class
|
36
|
+
# Class representing a board game.
|
66
37
|
class Boardgame
|
67
38
|
EXIT_INSTRUCTIONS ||= "Try a sample input or input 'back' to leave the " \
|
68
39
|
"tutorial. Type in 'exit' anytime to exit the game fully"
|
69
40
|
|
70
|
-
def initialize(board, instructions,
|
71
|
-
@
|
72
|
-
@player2 = Player.new(name2)
|
41
|
+
def initialize(board, instructions, names)
|
42
|
+
@players = names.map { |name| Player.new name }
|
73
43
|
@board = setup_board(board)
|
74
44
|
@instructions = instructions
|
75
45
|
@winner = nil
|
76
46
|
end
|
77
47
|
|
78
|
-
def self.play(do_onboarding: true)
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
48
|
+
def self.play(do_onboarding: true, num_players: 2)
|
49
|
+
names = []
|
50
|
+
num_players.times do |i|
|
51
|
+
puts "What is Player #{i}'s name?"
|
52
|
+
names.push(gets.chomp)
|
53
|
+
end
|
54
|
+
|
55
|
+
@game = new(names)
|
84
56
|
|
85
57
|
puts "Welcome to #{@game}!"
|
86
58
|
@game.onboarding if do_onboarding
|
@@ -88,17 +60,18 @@ module BoardgameEngine
|
|
88
60
|
@game.start
|
89
61
|
end
|
90
62
|
|
91
|
-
def to_s(game_name =
|
92
|
-
"#{game_name} between #{@
|
63
|
+
def to_s(game_name = 'boardgame')
|
64
|
+
"#{game_name} between #{@players.join(', ')}"
|
93
65
|
end
|
94
66
|
|
95
67
|
def onboarding
|
96
68
|
puts "Would you like a tutorial on how to play on this program? \n(y, n)"
|
69
|
+
|
97
70
|
case gets.chomp
|
98
|
-
when
|
71
|
+
when 'y'
|
99
72
|
tutorial
|
100
|
-
when
|
101
|
-
puts
|
73
|
+
when 'n'
|
74
|
+
puts 'Skipping Tutorial'
|
102
75
|
else
|
103
76
|
puts 'Please answer either "y" or "n"'
|
104
77
|
onboarding
|
@@ -108,14 +81,14 @@ module BoardgameEngine
|
|
108
81
|
def tutorial
|
109
82
|
puts @instructions + Boardgame::EXIT_INSTRUCTIONS
|
110
83
|
input = gets.chomp
|
111
|
-
until input ==
|
112
|
-
exit if input ==
|
113
|
-
puts valid_input?(input) ?
|
84
|
+
until input == 'back'
|
85
|
+
exit if input == 'exit'
|
86
|
+
puts valid_input?(input) ? 'Valid input!' : 'Invalid input'
|
114
87
|
input = gets.chomp
|
115
88
|
end
|
116
89
|
end
|
117
90
|
|
118
|
-
def start(turn = @
|
91
|
+
def start(turn = @players[0])
|
119
92
|
@turn = turn
|
120
93
|
@board.display
|
121
94
|
until @winner
|
@@ -127,15 +100,16 @@ module BoardgameEngine
|
|
127
100
|
|
128
101
|
protected
|
129
102
|
|
130
|
-
def
|
103
|
+
def get_valid_board_input(special_commands = [])
|
131
104
|
input = gets.chomp
|
132
|
-
until valid_input?(input)
|
133
|
-
exit if input == "exit"
|
134
|
-
return input if special_commands.include?(input)
|
135
105
|
|
136
|
-
|
106
|
+
until @board.valid_board_input?(input) || special_commands.include?(input)
|
107
|
+
exit if input == 'exit'
|
108
|
+
|
109
|
+
puts 'Invalid input. Try again'
|
137
110
|
input = gets.chomp
|
138
111
|
end
|
112
|
+
|
139
113
|
input
|
140
114
|
end
|
141
115
|
|
@@ -143,4 +117,62 @@ module BoardgameEngine
|
|
143
117
|
board.new
|
144
118
|
end
|
145
119
|
end
|
120
|
+
|
121
|
+
class Piece
|
122
|
+
attr_accessor :status
|
123
|
+
attr_reader :owner
|
124
|
+
|
125
|
+
def initialize(owner, name)
|
126
|
+
@status = 'alive'
|
127
|
+
@owner = owner
|
128
|
+
@name = name
|
129
|
+
end
|
130
|
+
|
131
|
+
def kill(other)
|
132
|
+
other.status = 'dead'
|
133
|
+
end
|
134
|
+
|
135
|
+
def to_s
|
136
|
+
@name.to_s
|
137
|
+
end
|
138
|
+
|
139
|
+
protected
|
140
|
+
|
141
|
+
def clear_diag_path?(row, col, end_row, end_col, board)
|
142
|
+
((end_row - row).abs == (end_col - col).abs) \
|
143
|
+
&& clear_path?(row, col, end_row, end_col, board)
|
144
|
+
end
|
145
|
+
|
146
|
+
def clear_horz_path?(row, col, end_row, end_col, board)
|
147
|
+
(end_row == row) && clear_path?(row, col, end_row, end_col, board)
|
148
|
+
end
|
149
|
+
|
150
|
+
def clear_vert_path?(row, col, end_row, end_col, board)
|
151
|
+
(end_col == col) && clear_path?(row, col, end_row, end_col, board)
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def next_cell(row, col, end_row, end_col)
|
157
|
+
row_move = 0
|
158
|
+
col_move = 0
|
159
|
+
|
160
|
+
col_move = (end_col - col) / (end_col - col).abs if end_col != col
|
161
|
+
row_move = (end_row - row) / (end_row - row).abs if end_row != row
|
162
|
+
|
163
|
+
[row + row_move, col + col_move]
|
164
|
+
end
|
165
|
+
|
166
|
+
def clear_path?(row, col, end_row, end_col, board)
|
167
|
+
current_tile = board.dig(row, col)
|
168
|
+
if (row == end_row) && (col == end_col)
|
169
|
+
current_tile.nil? || (current_tile.owner != @owner)
|
170
|
+
elsif current_tile.nil? || current_tile.equal?(self)
|
171
|
+
next_row, next_col = next_cell(row, col, end_row, end_col)
|
172
|
+
clear_path?(next_row, next_col, end_row, end_col, board)
|
173
|
+
else
|
174
|
+
false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
146
178
|
end
|
@@ -1,14 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'boardgame_engine/boardgame'
|
4
|
+
require 'boardgame_engine/game_modules'
|
5
|
+
require 'boardgame_engine/board_modules'
|
5
6
|
|
6
7
|
module SampleChess
|
7
8
|
class ChessBoard < BoardgameEngine::Board
|
9
|
+
include Boards::Grid
|
10
|
+
include Boards::MovablePiece
|
11
|
+
|
8
12
|
attr_reader :board
|
9
13
|
|
10
14
|
def initialize(player1, player2)
|
11
|
-
|
15
|
+
@board = generate_board(8, 8)
|
12
16
|
setup(player1, player2)
|
13
17
|
end
|
14
18
|
|
@@ -38,154 +42,100 @@ module SampleChess
|
|
38
42
|
end
|
39
43
|
|
40
44
|
class Chess < BoardgameEngine::Boardgame
|
41
|
-
include
|
45
|
+
include Games::CyclicalTurn
|
46
|
+
include Games::MovablePiece
|
42
47
|
|
43
|
-
def initialize(
|
44
|
-
@instructions =
|
45
|
-
"and column with a comma in between. See example below\n1, 1\n"
|
46
|
-
super(ChessBoard, @instructions,
|
48
|
+
def initialize(names)
|
49
|
+
@instructions = 'You can select spots on the board by inputting the row' \
|
50
|
+
" and column with a comma in between. See example below\n1, 1\n"
|
51
|
+
super(ChessBoard, @instructions, names)
|
47
52
|
end
|
48
53
|
|
49
54
|
def to_s
|
50
|
-
super(
|
55
|
+
super('chess')
|
51
56
|
end
|
52
57
|
|
53
58
|
private
|
54
59
|
|
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
60
|
def valid_piece?(piece)
|
61
61
|
!piece.nil? && piece.owner == @turn
|
62
62
|
end
|
63
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
64
|
def play_turn
|
89
|
-
puts "#{@turn}'s turn
|
90
|
-
|
91
|
-
|
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)
|
65
|
+
puts "#{@turn}'s turn"
|
66
|
+
killed = play_move
|
67
|
+
@winner = @turn if killed.is_a?(King)
|
98
68
|
change_turn
|
99
69
|
end
|
100
70
|
|
101
71
|
def setup_board(board)
|
102
|
-
board.new(@
|
72
|
+
board.new(@players[0], @players[1])
|
103
73
|
end
|
104
74
|
end
|
105
75
|
|
106
|
-
class
|
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
|
76
|
+
class Pawn < BoardgameEngine::Piece
|
167
77
|
def initialize(owner, front)
|
168
|
-
super(owner,
|
78
|
+
super(owner, 'p')
|
169
79
|
@first_move = true
|
170
80
|
@front = front
|
171
81
|
end
|
172
82
|
|
173
|
-
|
83
|
+
# Check whether the intended destination is a valid destination
|
84
|
+
#
|
85
|
+
# @param [Array<Integer, Integer>] start_location the start coords
|
86
|
+
# @param [Array<Integer, Integer>] end_location the intended destination
|
87
|
+
# @param [ChessBoard] board the chess board
|
88
|
+
#
|
89
|
+
# @return [<Type>] whether the pawn can move to the intended destination
|
90
|
+
def valid_move?(start_location, end_location, board)
|
91
|
+
row, col = start_location
|
92
|
+
end_row, end_col = end_location
|
93
|
+
|
94
|
+
# Checks if moving 1 (or 2 if its the first move) cell forward
|
174
95
|
return false unless valid_forward_move?(row, end_row)
|
175
96
|
|
176
|
-
if col == end_col
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
return other_piece && (other_piece.owner != @owner)
|
97
|
+
if col == end_col
|
98
|
+
valid_line_move?(end_row, end_col, board)
|
99
|
+
elsif (col - end_col).abs == 1 && (row + @front == end_row)
|
100
|
+
valid_diag_move?(end_row, end_col, board)
|
101
|
+
else
|
102
|
+
false
|
183
103
|
end
|
184
|
-
false
|
185
104
|
end
|
186
105
|
|
187
106
|
private
|
188
107
|
|
108
|
+
# Check if the pawn can move in a straight line forward to the specified
|
109
|
+
# coord
|
110
|
+
#
|
111
|
+
# @param [Integer] end_row the destination row number
|
112
|
+
# @param [Integer] end_col the destination column number
|
113
|
+
#
|
114
|
+
# @return [Boolean] whether the pawn can move or not
|
115
|
+
def valid_line_move?(end_row, end_col, board)
|
116
|
+
is_valid_dest = board.get_piece_at([end_row, end_col]).nil?
|
117
|
+
@first_move = false if is_valid_dest
|
118
|
+
is_valid_dest
|
119
|
+
end
|
120
|
+
|
121
|
+
# Check if the pawn can move in a diagonal line to the specified coord
|
122
|
+
#
|
123
|
+
# @param [Integer] end_row the destination row number
|
124
|
+
# @param [Integer] end_col the destination column number
|
125
|
+
#
|
126
|
+
# @return [Boolean] whether the pawn can move or not
|
127
|
+
def valid_diag_move?(end_row, end_col, board)
|
128
|
+
other_piece = board.get_piece_at([end_row, end_col])
|
129
|
+
@first_move = false if other_piece
|
130
|
+
other_piece && (other_piece.owner != @owner)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Check if the pawn movement is valid row-wise
|
134
|
+
#
|
135
|
+
# @param [Integer] row the row the pawn starts from
|
136
|
+
# @param [Integer] end_row the destination row
|
137
|
+
#
|
138
|
+
# @return [Boolean] whether the pawn's row movement is valid numerically
|
189
139
|
def valid_forward_move?(row, end_row)
|
190
140
|
if @first_move
|
191
141
|
(row + @front * 2 == end_row) || (row + @front == end_row)
|
@@ -195,60 +145,75 @@ module SampleChess
|
|
195
145
|
end
|
196
146
|
end
|
197
147
|
|
198
|
-
class Queen <
|
148
|
+
class Queen < BoardgameEngine::Piece
|
199
149
|
def initialize(owner)
|
200
|
-
super(owner,
|
150
|
+
super(owner, 'Q')
|
201
151
|
end
|
202
152
|
|
203
|
-
def valid_move?(
|
204
|
-
|
205
|
-
|
206
|
-
|
153
|
+
def valid_move?(start_location, end_location, board)
|
154
|
+
row, col = start_location
|
155
|
+
end_row, end_col = end_location
|
156
|
+
|
157
|
+
clear_diag_path?(row, col, end_row, end_col, board) \
|
158
|
+
|| clear_horz_path?(row, col, end_row, end_col, board) \
|
159
|
+
|| clear_vert_path?(row, col, end_row, end_col, board)
|
207
160
|
end
|
208
161
|
end
|
209
162
|
|
210
|
-
class Rook <
|
163
|
+
class Rook < BoardgameEngine::Piece
|
211
164
|
def initialize(owner)
|
212
|
-
super(owner,
|
165
|
+
super(owner, 'R')
|
213
166
|
end
|
214
167
|
|
215
|
-
def valid_move?(
|
216
|
-
|
217
|
-
|
168
|
+
def valid_move?(start_location, end_location, board)
|
169
|
+
row, col = start_location
|
170
|
+
end_row, end_col = end_location
|
171
|
+
|
172
|
+
clear_horz_path?(row, col, end_row, end_col, board) \
|
173
|
+
|| clear_vert_path?(row, col, end_row, end_col, board)
|
218
174
|
end
|
219
175
|
end
|
220
176
|
|
221
|
-
class Bishop <
|
177
|
+
class Bishop < BoardgameEngine::Piece
|
222
178
|
def initialize(owner)
|
223
|
-
super(owner,
|
179
|
+
super(owner, 'B')
|
224
180
|
end
|
225
181
|
|
226
|
-
def valid_move?(
|
227
|
-
|
182
|
+
def valid_move?(start_location, end_location, board)
|
183
|
+
row, col = start_location
|
184
|
+
end_row, end_col = end_location
|
185
|
+
|
186
|
+
clear_diag_path?(row, col, end_row, end_col, board)
|
228
187
|
end
|
229
188
|
end
|
230
189
|
|
231
|
-
class King <
|
190
|
+
class King < BoardgameEngine::Piece
|
232
191
|
def initialize(owner)
|
233
|
-
super(owner,
|
192
|
+
super(owner, 'K')
|
234
193
|
end
|
235
194
|
|
236
|
-
def valid_move?(
|
195
|
+
def valid_move?(start_location, end_location, board)
|
196
|
+
row, col = start_location
|
197
|
+
end_row, end_col = end_location
|
198
|
+
|
237
199
|
return false unless (row - end_row).abs == 1 && (col - end_col).abs == 1
|
238
200
|
|
239
|
-
|
240
|
-
||
|
241
|
-
||
|
201
|
+
clear_diag_path?(row, col, end_row, end_col, board) \
|
202
|
+
|| clear_horz_path?(row, col, end_row, end_col, board) \
|
203
|
+
|| clear_vert_path?(row, col, end_row, end_col, board)
|
242
204
|
end
|
243
205
|
end
|
244
206
|
|
245
|
-
class Knight <
|
207
|
+
class Knight < BoardgameEngine::Piece
|
246
208
|
def initialize(owner)
|
247
209
|
# K was already taken by king, so I had to choose N
|
248
|
-
super(owner,
|
210
|
+
super(owner, 'N')
|
249
211
|
end
|
250
212
|
|
251
|
-
def valid_move?(
|
213
|
+
def valid_move?(start_location, end_location, board)
|
214
|
+
row, col = start_location
|
215
|
+
end_row, end_col = end_location
|
216
|
+
|
252
217
|
within_movement(row, col, end_row, end_col) \
|
253
218
|
&& not_occupied(end_row, end_col, board)
|
254
219
|
end
|
@@ -1,12 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'boardgame_engine/boardgame'
|
4
|
+
require 'boardgame_engine/game_modules'
|
5
|
+
require 'boardgame_engine/board_modules'
|
5
6
|
|
6
7
|
module SampleConnect4
|
7
8
|
class Connect4Board < BoardgameEngine::Board
|
9
|
+
include Boards::Grid
|
10
|
+
|
8
11
|
def initialize
|
9
|
-
|
12
|
+
@board = generate_board(6, 7)
|
10
13
|
end
|
11
14
|
|
12
15
|
def display
|
@@ -21,57 +24,34 @@ module SampleConnect4
|
|
21
24
|
end
|
22
25
|
end
|
23
26
|
end
|
27
|
+
|
28
|
+
def valid_board_input?(input)
|
29
|
+
super(input, only_col: true)
|
30
|
+
end
|
24
31
|
end
|
25
32
|
|
26
33
|
class Connect4 < BoardgameEngine::Boardgame
|
27
|
-
include
|
34
|
+
include Games::CyclicalTurn
|
28
35
|
|
29
|
-
@instructions =
|
30
|
-
|
36
|
+
@instructions = 'You can select which column to drop you chip into by' \
|
37
|
+
' typing in the row number.'
|
31
38
|
|
32
|
-
def initialize(
|
33
|
-
super(Connect4Board, @instructions,
|
39
|
+
def initialize(names)
|
40
|
+
super(Connect4Board, @instructions, names)
|
34
41
|
end
|
35
42
|
|
36
43
|
def to_s
|
37
|
-
super(
|
44
|
+
super('connect-four')
|
38
45
|
end
|
39
46
|
|
40
47
|
private
|
41
48
|
|
42
|
-
def valid_input?(input)
|
43
|
-
input.match?(/[[:digit:]]/) && input.to_i.between?(0, 6)
|
44
|
-
end
|
45
|
-
|
46
49
|
def play_turn
|
47
|
-
puts "#{@turn}'s turn
|
48
|
-
col =
|
50
|
+
puts "#{@turn}'s turn\nChoose a column to drop your chip in"
|
51
|
+
col = get_valid_board_input.to_i
|
49
52
|
@board.drop_chip(col, @turn)
|
50
|
-
@winner = @turn if
|
53
|
+
@winner = @turn if @board.consecutive?
|
51
54
|
change_turn
|
52
55
|
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
56
|
end
|
77
57
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Module for different game rules
|
4
|
+
module Games
|
5
|
+
# Module for games with a cyclical turn system
|
6
|
+
module CyclicalTurn
|
7
|
+
# Changes the turn to the next player
|
8
|
+
def change_turn
|
9
|
+
idx = @players.find_index(@turn)
|
10
|
+
@turn = @players[(idx + 1) % @players.length]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Module for games where game pieces are moved
|
15
|
+
module MovablePiece
|
16
|
+
# Execute a single move according to player input
|
17
|
+
#
|
18
|
+
# @return [void]
|
19
|
+
def play_move
|
20
|
+
puts 'Select your piece'
|
21
|
+
piece, row, col = select_piece_from_input
|
22
|
+
puts "Select where to move \"#{piece}\" to. Type \"back\" to reselect piece"
|
23
|
+
dest = select_destination(piece, row, col)
|
24
|
+
return play_move if dest == 'back'
|
25
|
+
|
26
|
+
@board.move_piece([row, col], dest)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Select a piece on the board from user input
|
32
|
+
#
|
33
|
+
# @return [Array] the piece, and the piece's location
|
34
|
+
def select_piece_from_input
|
35
|
+
location = @board.parse_input(get_valid_board_input)
|
36
|
+
piece = @board.get_piece_at(location)
|
37
|
+
|
38
|
+
if valid_piece?(piece)
|
39
|
+
[piece, location]
|
40
|
+
else
|
41
|
+
puts 'Invalid piece. Try again'
|
42
|
+
select_piece_from_input
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Select a location on the board to move to from user input
|
47
|
+
#
|
48
|
+
# @param [<Type>] piece The piece to move
|
49
|
+
# @param [<Type>] start_location the starting location of the piece
|
50
|
+
#
|
51
|
+
# @return [<Type>] the location of the piece or the string literal 'back'
|
52
|
+
def select_destination(piece, start_location)
|
53
|
+
input = get_valid_board_input(['back'])
|
54
|
+
return 'back' if input == 'back'
|
55
|
+
|
56
|
+
end_location = @board.parse_input(input)
|
57
|
+
if piece.valid_move?(start_location, end_location, @board)
|
58
|
+
end_location
|
59
|
+
else
|
60
|
+
puts 'Invalid destination. Try again'
|
61
|
+
select_destination(piece, start_location)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/boardgame_engine.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'boardgame_engine/version'
|
4
|
+
require 'boardgame_engine/boardgame'
|
5
|
+
require 'boardgame_engine/game_modules'
|
6
|
+
require 'boardgame_engine/board_modules'
|
7
|
+
require 'boardgame_engine/sample_games'
|
4
8
|
|
5
|
-
|
6
|
-
require "boardgame_engine/multiplayergame"
|
7
|
-
require "boardgame_engine/sample_games"
|
8
|
-
|
9
|
-
module BoardgameEngine
|
10
|
-
class Error < StandardError; end
|
11
|
-
end
|
9
|
+
module BoardgameEngine 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.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- FortHoney
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -24,7 +24,7 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.2'
|
27
|
-
description:
|
27
|
+
description:
|
28
28
|
email:
|
29
29
|
- castlehoneyjung@gmail.com
|
30
30
|
executables: []
|
@@ -39,10 +39,11 @@ files:
|
|
39
39
|
- Rakefile
|
40
40
|
- boardgame_engine.gemspec
|
41
41
|
- lib/boardgame_engine.rb
|
42
|
+
- lib/boardgame_engine/board_modules.rb
|
42
43
|
- lib/boardgame_engine/boardgame.rb
|
43
44
|
- lib/boardgame_engine/chess.rb
|
44
45
|
- lib/boardgame_engine/connect4.rb
|
45
|
-
- lib/boardgame_engine/
|
46
|
+
- lib/boardgame_engine/game_modules.rb
|
46
47
|
- lib/boardgame_engine/sample_games.rb
|
47
48
|
- lib/boardgame_engine/version.rb
|
48
49
|
- sig/boardgame_engine.rbs
|
@@ -52,7 +53,7 @@ metadata:
|
|
52
53
|
homepage_uri: https://github.com/Forthoney/boardgame_engine
|
53
54
|
source_code_uri: https://github.com/Forthoney/boardgame_engine
|
54
55
|
changelog_uri: https://github.com/Forthoney/boardgame_engine/blob/main/CHANGELOG.md
|
55
|
-
post_install_message:
|
56
|
+
post_install_message:
|
56
57
|
rdoc_options: []
|
57
58
|
require_paths:
|
58
59
|
- lib
|
@@ -60,15 +61,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
61
|
requirements:
|
61
62
|
- - ">="
|
62
63
|
- !ruby/object:Gem::Version
|
63
|
-
version:
|
64
|
+
version: 3.0.0
|
64
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
66
|
requirements:
|
66
67
|
- - ">="
|
67
68
|
- !ruby/object:Gem::Version
|
68
69
|
version: '0'
|
69
70
|
requirements: []
|
70
|
-
rubygems_version: 3.
|
71
|
-
signing_key:
|
71
|
+
rubygems_version: 3.4.4
|
72
|
+
signing_key:
|
72
73
|
specification_version: 4
|
73
74
|
summary: A gem that makes digitizing/creating board games to be played on the terminal
|
74
75
|
quick and easy.
|