boardgame_engine 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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