boardgame_engine 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e57f58857092b8b08b48de9bafcf09a08297302fcbe695afde86125615d07f1
4
- data.tar.gz: c7de6b02681a7b7def284e31186fc61281fe9ae41dfe75464dc0a5e83b5bde9a
3
+ metadata.gz: c8a00224a55f64e290296e59c90686e9984141c43b3ce1d588cbe2a6f382d906
4
+ data.tar.gz: 83ec3bd763ae34688b1f99f8e2c54d9bc9c0229430fc80ae5211db81a9ce47a0
5
5
  SHA512:
6
- metadata.gz: 2155a5ca5a0c871e277a7a0e4beb9ea25b86a17ada30481d4f37b6104385c300095ef61cd694b860cadc72ab9230f6f7d199934a15c96de3278b22756174b07c
7
- data.tar.gz: accc0b89071a919f466be01d1ecd68bc4f2fe3096817ff6340b161e731beabe1e05142399af352922cf7ac521ee95711fc6681068cf5b083c3e7ba9fb51ca02f
6
+ metadata.gz: c3d1d0647a5ff2b85b619aa85f72dc6a1f3638074a2333b22fed2535239c0feb64c43e4ac97eb98bbff45c77ebf513bd9d401299b8046483a2520dca005fdf1f
7
+ data.tar.gz: eb8768b54c655792cf5f5270ddcc30867b1afd4e80d8382e97704873742b1dd8964925ed1683eff327b386c0d0f9d2aebf454ff1ecfe4cd1bed326dfd1cb3c41
data/.rubocop.yml CHANGED
@@ -1,13 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.1
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
2
+ TargetRubyVersion: 3.0
11
3
 
12
4
  Layout/LineLength:
13
5
  Max: 120
data/Gemfile CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in boardgame_engine.gemspec
6
6
  gemspec
7
7
 
8
- gem "rake", "~> 13.0"
8
+ gem 'rake', '~> 13.0'
9
9
 
10
- gem "rubocop", "~> 1.21"
10
+ gem 'rubocop', '~> 1.21'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boardgame_engine (0.1.0)
4
+ boardgame_engine (0.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rubocop/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
5
 
6
6
  RuboCop::RakeTask.new
7
7
 
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/boardgame_engine/version"
3
+ require_relative 'lib/boardgame_engine/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "boardgame_engine"
6
+ spec.name = 'boardgame_engine'
7
7
  spec.version = BoardgameEngine::VERSION
8
- spec.authors = ["FortHoney"]
9
- spec.email = ["castlehoneyjung@gmail.com"]
8
+ spec.authors = ['FortHoney']
9
+ spec.email = ['castlehoneyjung@gmail.com']
10
10
 
11
- spec.summary = "A gem that makes digitizing/creating board games to be played on the terminal quick and easy."
12
- spec.homepage = "https://github.com/Forthoney/boardgame_engine"
13
- spec.required_ruby_version = ">= 2.7.0"
11
+ spec.summary = 'A gem that makes digitizing/creating board games to be played on the terminal quick and easy.'
12
+ spec.homepage = 'https://github.com/Forthoney/boardgame_engine'
13
+ spec.required_ruby_version = '>= 3.0.0'
14
14
 
15
15
  # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
16
16
 
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/Forthoney/boardgame_engine"
19
- spec.metadata["changelog_uri"] = "https://github.com/Forthoney/boardgame_engine/blob/main/CHANGELOG.md"
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/Forthoney/boardgame_engine'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/Forthoney/boardgame_engine/blob/main/CHANGELOG.md'
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
22
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -25,11 +25,11 @@ Gem::Specification.new do |spec|
25
25
  (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
26
  end
27
27
  end
28
- spec.bindir = "exe"
28
+ spec.bindir = 'exe'
29
29
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
- spec.require_paths = ["lib"]
30
+ spec.require_paths = ['lib']
31
31
 
32
- spec.add_development_dependency "rspec", "~> 3.2"
32
+ spec.add_development_dependency 'rspec', '~> 3.2'
33
33
 
34
34
  # For more information and examples about making a new gem, check out our
35
35
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -0,0 +1,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
@@ -3,142 +3,176 @@
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
- # Class representing a physical board comprised of a grid in a board game. It
7
- # acts as both the View and Model if the project were to be compared to a MVC
8
- # model. It plays both roles as the board in a board game not only stores data,
9
- # but also IS the data that must be shown to the players.
10
- class Board
11
- attr_reader :board
12
-
13
- def initialize(row, col)
14
- @board = Array.new(row) { Array.new(col) { nil } }
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
+ private
15
+
16
+ def spot_playable?(piece, row, col)
17
+ piece.possible_moves.include? [row, col]
18
+ end
15
19
  end
16
20
 
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) }
21
+ # Class representing a player in a game
22
+ class Player
23
+ attr_reader :name
24
+
25
+ # Creates a player object with the given name
26
+ # @param [String] name
27
+ def initialize(name)
28
+ @name = name
22
29
  end
23
- return unless show_col
24
30
 
25
- column_spacer = show_row ? " " : ""
26
- puts(@board[0].each_index.reduce(column_spacer) do |str, idx|
27
- str + " #{idx} "
28
- end)
31
+ def to_s
32
+ @name.to_s
33
+ end
29
34
  end
30
35
 
31
- def move_piece(start_row, start_col, end_row, end_col)
32
- piece = @board[start_row][start_col]
33
- @board[start_row][start_col] = nil
34
- destination = @board[end_row][end_col]
35
- @board[end_row][end_col] = piece
36
+ # Class representing a board game.
37
+ class Boardgame
38
+ EXIT_INSTRUCTIONS ||= "Try a sample input or input 'back' to leave the " \
39
+ "tutorial. Type in 'exit' anytime to exit the game fully"
36
40
 
37
- destination
38
- end
41
+ def initialize(board, instructions, names)
42
+ @players = names.map { |name| Player.new name }
43
+ @board = setup_board(board)
44
+ @instructions = instructions
45
+ @winner = nil
46
+ end
39
47
 
40
- private
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
41
54
 
42
- def format_row(row)
43
- row.map { |elem| "[#{elem.nil? ? " " : elem}]" }.join
44
- end
55
+ @game = new(names)
45
56
 
46
- def spot_playable?(piece, row, col)
47
- piece.possible_moves.include? [row, col]
48
- end
49
- end
57
+ puts "Welcome to #{@game}!"
58
+ @game.onboarding if do_onboarding
59
+ puts "Starting #{@game}..."
60
+ @game.start
61
+ end
50
62
 
51
- # Class representing a player in a game
52
- class Player
53
- attr_reader :name
63
+ def to_s(game_name = 'boardgame')
64
+ "#{game_name} between #{@players.join(', ')}"
65
+ end
54
66
 
55
- def initialize(name)
56
- @name = name
57
- end
67
+ def onboarding
68
+ puts "Would you like a tutorial on how to play on this program? \n(y, n)"
69
+
70
+ case gets.chomp
71
+ when 'y'
72
+ tutorial
73
+ when 'n'
74
+ puts 'Skipping Tutorial'
75
+ else
76
+ puts 'Please answer either "y" or "n"'
77
+ onboarding
78
+ end
79
+ end
58
80
 
59
- def to_s
60
- @name.to_s
61
- end
62
- end
81
+ def tutorial
82
+ puts @instructions + Boardgame::EXIT_INSTRUCTIONS
83
+ input = gets.chomp
84
+ until input == 'back'
85
+ exit if input == 'exit'
86
+ puts valid_input?(input) ? 'Valid input!' : 'Invalid input'
87
+ input = gets.chomp
88
+ end
89
+ end
63
90
 
64
- # Class for running a board game.
65
- class Boardgame
66
- EXIT_INSTRUCTIONS ||= "Try a sample input or input 'back' to leave the " \
67
- "tutorial. Type in 'exit' anytime to exit the game fully"
68
-
69
- def initialize(board, instructions, name1 = "Player 1", name2 = "Player 2")
70
- @player1 = Player.new(name1)
71
- @player2 = Player.new(name2)
72
- @board = setup_board(board)
73
- @instructions = instructions
74
- @winner = nil
75
- end
91
+ def start(turn = @players[0])
92
+ @turn = turn
93
+ @board.display
94
+ until @winner
95
+ play_turn
96
+ @board.display
97
+ end
98
+ puts "#{@winner} wins!"
99
+ end
76
100
 
77
- def self.play(do_onboarding: true)
78
- puts "What is Player 1's name?"
79
- player1 = gets.chomp
80
- puts "What is Player 2's name?"
81
- player2 = gets.chomp
82
- @game = new(player1, player2)
83
-
84
- puts "Welcome to #{@game}!"
85
- @game.onboarding if do_onboarding
86
- puts "Starting #{@game}..."
87
- @game.start
88
- end
101
+ protected
89
102
 
90
- def to_s(game_name = "boardgame")
91
- "#{game_name} between #{@player1} and #{@player2}"
92
- end
103
+ def get_valid_board_input(special_commands = [])
104
+ input = gets.chomp
93
105
 
94
- def onboarding
95
- puts "Would you like a tutorial on how to play on this program? \n(y, n)"
96
- case gets.chomp
97
- when "y"
98
- tutorial
99
- when "n"
100
- puts "Skipping tutorial"
101
- else
102
- puts 'Please answer either "y" or "n"'
103
- onboarding
106
+ until @board.valid_board_input?(input) || special_commands.include?(input)
107
+ exit if input == 'exit'
108
+
109
+ puts 'Invalid input. Try again'
110
+ input = gets.chomp
111
+ end
112
+
113
+ input
104
114
  end
105
- end
106
115
 
107
- def tutorial
108
- puts @instructions + Boardgame::EXIT_INSTRUCTIONS
109
- input = gets.chomp
110
- until input == "back"
111
- exit if input == "exit"
112
- puts valid_input?(input) ? "Valid input!" : "Invalid input"
113
- input = gets.chomp
116
+ def setup_board(board)
117
+ board.new
114
118
  end
115
119
  end
116
120
 
117
- def start(turn = @player1)
118
- @turn = turn
119
- @board.display
120
- until @winner
121
- play_turn
122
- @board.display
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
123
129
  end
124
- puts "#{@winner} wins!"
125
- end
126
130
 
127
- protected
131
+ def kill(other)
132
+ other.status = 'dead'
133
+ end
128
134
 
129
- def proper_format_input(special_commands = [])
130
- input = gets.chomp
131
- until valid_input?(input)
132
- exit if input == "exit"
133
- return input if special_commands.include?(input)
135
+ def to_s
136
+ @name.to_s
137
+ end
134
138
 
135
- puts "Input is in the wrong format or out of bounds. Try again"
136
- input = gets.chomp
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)
137
144
  end
138
- input
139
- end
140
145
 
141
- def setup_board(board)
142
- board.new
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
143
177
  end
144
178
  end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'boardgame_engine/boardgame'
4
+ require 'boardgame_engine/game_modules'
5
+ require 'boardgame_engine/board_modules'
6
+
7
+ module SampleChess
8
+ class ChessBoard < BoardgameEngine::Board
9
+ include Boards::Grid
10
+ include Boards::MovablePiece
11
+
12
+ attr_reader :board
13
+
14
+ def initialize(player1, player2)
15
+ @board = generate_board(8, 8)
16
+ setup(player1, player2)
17
+ end
18
+
19
+ def display
20
+ super(show_row: true, show_col: true)
21
+ end
22
+
23
+ private
24
+
25
+ def setup(player1, player2)
26
+ set_pawns(player1, player2)
27
+ set_non_pawns(player1, player2)
28
+ end
29
+
30
+ def set_pawns(player1, player2)
31
+ @board[1] = @board[1].map { Pawn.new(player1, 1) }
32
+ @board[-2] = @board[-2].map { Pawn.new(player2, -1) }
33
+ end
34
+
35
+ def set_non_pawns(player1, player2)
36
+ pieces = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
37
+ pieces.each_with_index do |piece, idx|
38
+ @board[0][idx] = piece.new(player1)
39
+ @board[7][7 - idx] = piece.new(player2)
40
+ end
41
+ end
42
+ end
43
+
44
+ class Chess < BoardgameEngine::Boardgame
45
+ include Games::CyclicalTurn
46
+ include Games::MovablePiece
47
+
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)
52
+ end
53
+
54
+ def to_s
55
+ super('chess')
56
+ end
57
+
58
+ private
59
+
60
+ def valid_piece?(piece)
61
+ !piece.nil? && piece.owner == @turn
62
+ end
63
+
64
+ def play_turn
65
+ puts "#{@turn}'s turn"
66
+ killed = play_move
67
+ @winner = @turn if killed.is_a?(King)
68
+ change_turn
69
+ end
70
+
71
+ def setup_board(board)
72
+ board.new(@players[0], @players[1])
73
+ end
74
+ end
75
+
76
+ class Pawn < BoardgameEngine::Piece
77
+ def initialize(owner, front)
78
+ super(owner, 'p')
79
+ @first_move = true
80
+ @front = front
81
+ end
82
+
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
95
+ return false unless valid_forward_move?(row, end_row)
96
+
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
103
+ end
104
+ end
105
+
106
+ private
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
139
+ def valid_forward_move?(row, end_row)
140
+ if @first_move
141
+ (row + @front * 2 == end_row) || (row + @front == end_row)
142
+ else
143
+ row + @front == end_row
144
+ end
145
+ end
146
+ end
147
+
148
+ class Queen < BoardgameEngine::Piece
149
+ def initialize(owner)
150
+ super(owner, 'Q')
151
+ end
152
+
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)
160
+ end
161
+ end
162
+
163
+ class Rook < BoardgameEngine::Piece
164
+ def initialize(owner)
165
+ super(owner, 'R')
166
+ end
167
+
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)
174
+ end
175
+ end
176
+
177
+ class Bishop < BoardgameEngine::Piece
178
+ def initialize(owner)
179
+ super(owner, 'B')
180
+ end
181
+
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)
187
+ end
188
+ end
189
+
190
+ class King < BoardgameEngine::Piece
191
+ def initialize(owner)
192
+ super(owner, 'K')
193
+ end
194
+
195
+ def valid_move?(start_location, end_location, board)
196
+ row, col = start_location
197
+ end_row, end_col = end_location
198
+
199
+ return false unless (row - end_row).abs == 1 && (col - end_col).abs == 1
200
+
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)
204
+ end
205
+ end
206
+
207
+ class Knight < BoardgameEngine::Piece
208
+ def initialize(owner)
209
+ # K was already taken by king, so I had to choose N
210
+ super(owner, 'N')
211
+ end
212
+
213
+ def valid_move?(start_location, end_location, board)
214
+ row, col = start_location
215
+ end_row, end_col = end_location
216
+
217
+ within_movement(row, col, end_row, end_col) \
218
+ && not_occupied(end_row, end_col, board)
219
+ end
220
+
221
+ private
222
+
223
+ def within_movement(row, col, end_row, end_col)
224
+ ((row - end_row).abs == 2 and (col - end_col).abs == 1) \
225
+ || ((row - end_row).abs == 1 and (col - end_col).abs == 2)
226
+ end
227
+
228
+ def not_occupied(end_row, end_col, board)
229
+ spot = board.dig(end_row, end_col)
230
+ spot.nil? || spot.owner != @owner
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'boardgame_engine/boardgame'
4
+ require 'boardgame_engine/game_modules'
5
+ require 'boardgame_engine/board_modules'
6
+
7
+ module SampleConnect4
8
+ class Connect4Board < BoardgameEngine::Board
9
+ include Boards::Grid
10
+
11
+ def initialize
12
+ @board = generate_board(6, 7)
13
+ end
14
+
15
+ def display
16
+ super(show_col: true)
17
+ end
18
+
19
+ def drop_chip(col, owner)
20
+ @board.reverse_each do |row|
21
+ if row[col].nil?
22
+ row[col] = owner
23
+ break
24
+ end
25
+ end
26
+ end
27
+
28
+ def valid_board_input?(input)
29
+ super(input, only_col: true)
30
+ end
31
+ end
32
+
33
+ class Connect4 < BoardgameEngine::Boardgame
34
+ include Games::CyclicalTurn
35
+
36
+ @instructions = 'You can select which column to drop you chip into by' \
37
+ ' typing in the row number.'
38
+
39
+ def initialize(names)
40
+ super(Connect4Board, @instructions, names)
41
+ end
42
+
43
+ def to_s
44
+ super('connect-four')
45
+ end
46
+
47
+ private
48
+
49
+ def play_turn
50
+ puts "#{@turn}'s turn\nChoose a column to drop your chip in"
51
+ col = get_valid_board_input.to_i
52
+ @board.drop_chip(col, @turn)
53
+ @winner = @turn if @board.consecutive?
54
+ change_turn
55
+ end
56
+ end
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'boardgame_engine/chess'
4
+ require 'boardgame_engine/connect4'
5
+
6
+ module SampleGames
7
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BoardgameEngine
4
- VERSION = "0.1.0"
4
+ VERSION = '0.2.0'
5
5
  end
@@ -1,12 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "boardgame_engine/version"
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
- require_relative "boardgame_engine/boardgame"
6
- require_relative "boardgame_engine/multiplayergame"
7
- require_relative "boardgame_engine/chess/chess"
8
- require_relative "boardgame_engine/connect4/connect4"
9
-
10
- module BoardgameEngine
11
- class Error < StandardError; end
12
- 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.1.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: 2022-10-05 00:00:00.000000000 Z
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,11 +39,12 @@ 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
- - lib/boardgame_engine/chess/chess.rb
44
- - lib/boardgame_engine/chess/chess_pieces.rb
45
- - lib/boardgame_engine/connect4/connect4.rb
46
- - lib/boardgame_engine/multiplayergame.rb
44
+ - lib/boardgame_engine/chess.rb
45
+ - lib/boardgame_engine/connect4.rb
46
+ - lib/boardgame_engine/game_modules.rb
47
+ - lib/boardgame_engine/sample_games.rb
47
48
  - lib/boardgame_engine/version.rb
48
49
  - sig/boardgame_engine.rbs
49
50
  homepage: https://github.com/Forthoney/boardgame_engine
@@ -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: 2.7.0
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.3.7
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.
@@ -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
@@ -1,5 +0,0 @@
1
- module TwoPlayers
2
- def change_turn
3
- @turn = @turn == @player1 ? @player2 : @player1
4
- end
5
- end