just_xiangqi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,286 @@
1
+ require 'just_xiangqi/errors/no_piece_error'
2
+ require 'just_xiangqi/errors/not_players_turn_error'
3
+ require 'just_xiangqi/errors/off_board_error'
4
+ require 'just_xiangqi/errors/invalid_move_error'
5
+ require 'just_xiangqi/errors/moved_into_check_error'
6
+ require 'just_xiangqi/errors/piece_not_found_error'
7
+ require 'just_xiangqi/errors/square_occupied_error'
8
+ require 'just_xiangqi/errors/no_legal_moves_error'
9
+ require 'just_xiangqi/square_set'
10
+
11
+ module JustXiangqi
12
+
13
+ # = Game State
14
+ #
15
+ # Represents a game of Xiangqi in progress.
16
+ class GameState
17
+ def initialize(current_player_number: , squares: [])
18
+ @current_player_number = current_player_number
19
+ @squares = if squares.is_a?(SquareSet)
20
+ squares
21
+ else
22
+ SquareSet.new(squares: squares)
23
+ end
24
+ end
25
+
26
+ attr_reader :current_player_number, :squares, :errors
27
+
28
+ # Instantiates a new GameState object in the starting position.
29
+ #
30
+ # @return [GameState]
31
+ def self.default
32
+ new(
33
+ current_player_number: 1,
34
+ squares: [
35
+ { id: 'a10', x: 0, y: 0, piece: { id: 1, player_number: 2, type: 'ju' } },
36
+ { id: 'b10', x: 1, y: 0, piece: { id: 2, player_number: 2, type: 'ma' } },
37
+ { id: 'c10', x: 2, y: 0, piece: { id: 3, player_number: 2, type: 'xiang' } },
38
+ { id: 'd10', x: 3, y: 0, piece: { id: 4, player_number: 2, type: 'shi' } },
39
+ { id: 'e10', x: 4, y: 0, piece: { id: 5, player_number: 2, type: 'jiang' } },
40
+ { id: 'f10', x: 5, y: 0, piece: { id: 6, player_number: 2, type: 'shi' } },
41
+ { id: 'g10', x: 6, y: 0, piece: { id: 7, player_number: 2, type: 'xiang' } },
42
+ { id: 'h10', x: 7, y: 0, piece: { id: 8, player_number: 2, type: 'ma' } },
43
+ { id: 'i10', x: 8, y: 0, piece: { id: 9, player_number: 2, type: 'ju' } },
44
+
45
+ { id: 'a9', x: 0, y: 1, piece: nil },
46
+ { id: 'b9', x: 1, y: 1, piece: nil },
47
+ { id: 'c9', x: 2, y: 1, piece: nil },
48
+ { id: 'd9', x: 3, y: 1, piece: nil },
49
+ { id: 'e9', x: 4, y: 1, piece: nil },
50
+ { id: 'f9', x: 5, y: 1, piece: nil },
51
+ { id: 'g9', x: 6, y: 1, piece: nil },
52
+ { id: 'h9', x: 7, y: 1, piece: nil },
53
+ { id: 'i9', x: 8, y: 1, piece: nil },
54
+
55
+ { id: 'a8', x: 0, y: 2, piece: nil },
56
+ { id: 'b8', x: 1, y: 2, piece: { id: 10, player_number: 2, type: 'pao' } },
57
+ { id: 'c8', x: 2, y: 2, piece: nil },
58
+ { id: 'd8', x: 3, y: 2, piece: nil },
59
+ { id: 'e8', x: 4, y: 2, piece: nil },
60
+ { id: 'f8', x: 5, y: 2, piece: nil },
61
+ { id: 'g8', x: 6, y: 2, piece: nil },
62
+ { id: 'h8', x: 7, y: 2, piece: { id: 11, player_number: 2, type: 'pao' } },
63
+ { id: 'i8', x: 8, y: 2, piece: nil },
64
+
65
+ { id: 'a7', x: 0, y: 3, piece: { id: 12, player_number: 2, type: 'zu' } },
66
+ { id: 'b7', x: 1, y: 3, piece: nil },
67
+ { id: 'c7', x: 2, y: 3, piece: { id: 13, player_number: 2, type: 'zu' } },
68
+ { id: 'd7', x: 3, y: 3, piece: nil },
69
+ { id: 'e7', x: 4, y: 3, piece: { id: 14, player_number: 2, type: 'zu' } },
70
+ { id: 'f7', x: 5, y: 3, piece: nil },
71
+ { id: 'g7', x: 6, y: 3, piece: { id: 15, player_number: 2, type: 'zu' } },
72
+ { id: 'h7', x: 7, y: 3, piece: nil },
73
+ { id: 'i7', x: 8, y: 3, piece: { id: 16, player_number: 2, type: 'zu' } },
74
+
75
+ { id: 'a6', x: 0, y: 4, piece: nil },
76
+ { id: 'b6', x: 1, y: 4, piece: nil },
77
+ { id: 'c6', x: 2, y: 4, piece: nil },
78
+ { id: 'd6', x: 3, y: 4, piece: nil },
79
+ { id: 'e6', x: 4, y: 4, piece: nil },
80
+ { id: 'f6', x: 5, y: 4, piece: nil },
81
+ { id: 'g6', x: 6, y: 4, piece: nil },
82
+ { id: 'h6', x: 7, y: 4, piece: nil },
83
+ { id: 'i6', x: 8, y: 4, piece: nil },
84
+
85
+ { id: 'a5', x: 0, y: 5, piece: nil },
86
+ { id: 'b5', x: 1, y: 5, piece: nil },
87
+ { id: 'c5', x: 2, y: 5, piece: nil },
88
+ { id: 'd5', x: 3, y: 5, piece: nil },
89
+ { id: 'e5', x: 4, y: 5, piece: nil },
90
+ { id: 'f5', x: 5, y: 5, piece: nil },
91
+ { id: 'g5', x: 6, y: 5, piece: nil },
92
+ { id: 'h5', x: 7, y: 5, piece: nil },
93
+ { id: 'i5', x: 8, y: 5, piece: nil },
94
+
95
+ { id: 'a4', x: 0, y: 6, piece: { id: 17, player_number: 1, type: 'zu' } },
96
+ { id: 'b4', x: 1, y: 6, piece: nil },
97
+ { id: 'c4', x: 2, y: 6, piece: { id: 17, player_number: 1, type: 'zu' } },
98
+ { id: 'd4', x: 3, y: 6, piece: nil },
99
+ { id: 'e4', x: 4, y: 6, piece: { id: 17, player_number: 1, type: 'zu' } },
100
+ { id: 'f4', x: 5, y: 6, piece: nil },
101
+ { id: 'g4', x: 6, y: 6, piece: { id: 17, player_number: 1, type: 'zu' } },
102
+ { id: 'h4', x: 7, y: 6, piece: nil },
103
+ { id: 'i4', x: 8, y: 6, piece: { id: 17, player_number: 1, type: 'zu' } },
104
+
105
+ { id: 'a3', x: 0, y: 7, piece: nil },
106
+ { id: 'b3', x: 1, y: 7, piece: { id: 18, player_number: 1, type: 'pao' } },
107
+ { id: 'c3', x: 2, y: 7, piece: nil },
108
+ { id: 'd3', x: 3, y: 7, piece: nil },
109
+ { id: 'e3', x: 4, y: 7, piece: nil },
110
+ { id: 'f3', x: 5, y: 7, piece: nil },
111
+ { id: 'g3', x: 6, y: 7, piece: nil },
112
+ { id: 'h3', x: 7, y: 7, piece: { id: 19, player_number: 1, type: 'pao' } },
113
+ { id: 'i3', x: 8, y: 7, piece: nil },
114
+
115
+ { id: 'a2', x: 0, y: 8, piece: nil },
116
+ { id: 'b2', x: 1, y: 8, piece: nil },
117
+ { id: 'c2', x: 2, y: 8, piece: nil },
118
+ { id: 'd2', x: 3, y: 8, piece: nil },
119
+ { id: 'e2', x: 4, y: 8, piece: nil },
120
+ { id: 'f2', x: 5, y: 8, piece: nil },
121
+ { id: 'g2', x: 6, y: 8, piece: nil },
122
+ { id: 'h2', x: 7, y: 8, piece: nil },
123
+ { id: 'i2', x: 8, y: 8, piece: nil },
124
+
125
+ { id: 'a1', x: 0, y: 9, piece: { id: 20, player_number: 1, type: 'ju' } },
126
+ { id: 'b1', x: 1, y: 9, piece: { id: 21, player_number: 1, type: 'ma' } },
127
+ { id: 'c1', x: 2, y: 9, piece: { id: 22, player_number: 1, type: 'xiang' } },
128
+ { id: 'd1', x: 3, y: 9, piece: { id: 23, player_number: 1, type: 'shi' } },
129
+ { id: 'e1', x: 4, y: 9, piece: { id: 24, player_number: 1, type: 'jiang' } },
130
+ { id: 'f1', x: 5, y: 9, piece: { id: 25, player_number: 1, type: 'shi' } },
131
+ { id: 'g1', x: 6, y: 9, piece: { id: 26, player_number: 1, type: 'xiang' } },
132
+ { id: 'h1', x: 7, y: 9, piece: { id: 27, player_number: 1, type: 'ma' } },
133
+ { id: 'i1', x: 8, y: 9, piece: { id: 28, player_number: 1, type: 'ju' } }
134
+ ]
135
+ )
136
+ end
137
+
138
+ # serializes the game state as ahash
139
+ #
140
+ # @return [Hash]
141
+ def as_json
142
+ {
143
+ current_player_number: current_player_number,
144
+ squares: squares.as_json,
145
+ }
146
+ end
147
+
148
+ # deep clone of the game state
149
+ #
150
+ # @return [GameState]
151
+ def clone
152
+ self.class.new(**as_json)
153
+ end
154
+
155
+ # Moves a piece owned by the player, from one square, to another.
156
+ #
157
+ # It has an option to promote the moving piece.
158
+ # It moves the piece and returns true if the move is valid and it's the player's turn.
159
+ # It returns false otherwise.
160
+ #
161
+ # ==== Example:
162
+ # # Moves a piece from a square to perform a move
163
+ # game_state.move(1, '77', '78')
164
+ #
165
+ # @param [Fixnum] player_number
166
+ # the player number, 1 or 2.
167
+ #
168
+ # @param [String] from_id
169
+ # the id of the from square
170
+ #
171
+ # @param [String] to_id
172
+ # the id of the to square
173
+ #
174
+ # @return [Boolean]
175
+ def move(player_number, from_id, to_id)
176
+ @errors = []
177
+
178
+ from = squares.find_by_id(from_id)
179
+ to = squares.find_by_id(to_id)
180
+
181
+ if current_player_number != player_number
182
+ @errors.push JustXiangqi::NotPlayersTurnError.new
183
+ elsif from.unoccupied?
184
+ @errors.push JustXiangqi::NoPieceError.new
185
+ elsif to.nil?
186
+ @errors.push JustXiangqi::OffBoardError.new
187
+ elsif from.piece.can_move?(from, to, self)
188
+
189
+ duplicate = self.clone
190
+ duplicate.perform_complete_move(player_number, from_id, to_id)
191
+
192
+ if duplicate.in_check?(current_player_number)
193
+ @errors.push JustXiangqi::MovedIntoCheckError.new
194
+ else
195
+ perform_complete_move(player_number, from_id, to_id)
196
+ end
197
+ else
198
+ @errors.push JustXiangqi::InvalidMoveError.new
199
+ end
200
+
201
+ @errors.empty?
202
+ end
203
+
204
+ # The player number of the winner. It returns nil if there is no winner.
205
+ #
206
+ # @return [Fixnum,NilClass]
207
+ def winner
208
+ case
209
+ when in_checkmate?(1)
210
+ 2
211
+ when in_checkmate?(2)
212
+ 1
213
+ else
214
+ nil
215
+ end
216
+ end
217
+
218
+ # Moves a piece owned by the player, from one square, to another, with the option to promote.
219
+ #
220
+ # It moves the piece and returns true if the move is valid and it's the player's turn.
221
+ # It returns false otherwise.
222
+ def perform_complete_move(player_number, from_id, to_id)
223
+ from = squares.find_by_id(from_id)
224
+ to = squares.find_by_id(to_id)
225
+
226
+ captured = to.occupied? ? to : nil
227
+
228
+ @last_change = { type: 'move', data: { player_number: player_number, from: from_id, to: to_id } }
229
+
230
+ perform_move(player_number, from, to, captured)
231
+
232
+ pass_turn
233
+ end
234
+
235
+ def in_check?(player_number)
236
+ jiang_square = squares.find_jiang_for_player(player_number)
237
+ threatened_by = squares.threatened_by(opposing_player_number(player_number), self)
238
+ threatened_by.include?(jiang_square)
239
+ end
240
+
241
+ def in_checkmate?(player_number)
242
+ (in_check?(player_number) || non_jiang_pieces_cannot_move?(player_number)) && jiang_cannot_move?(player_number)
243
+ end
244
+
245
+ private
246
+
247
+ def non_jiang_pieces_cannot_move?(player_number)
248
+ squares.occupied_by_player(player_number).excluding_piece(JustXiangqi::Jiang).all? { |s| s.piece.destinations(s, self).empty? }
249
+ end
250
+
251
+ def jiang_cannot_move?(player_number)
252
+ jiang_square = squares.find_jiang_for_player(player_number)
253
+ destinations = jiang_square.piece.destinations(jiang_square, self)
254
+ destinations.all? do |destination|
255
+ duplicate = self.clone
256
+ duplicate.perform_complete_move(player_number, jiang_square.id, destination.id)
257
+ duplicate.in_check?(player_number)
258
+ end
259
+ end
260
+
261
+ def pass_turn
262
+ @current_player_number = opposing_player_number
263
+ end
264
+
265
+ def opposing_player_number(player_number = nil)
266
+ if player_number
267
+ other_player_number(player_number)
268
+ else
269
+ other_player_number(@current_player_number)
270
+ end
271
+ end
272
+
273
+ def other_player_number(player_number)
274
+ (player_number == 1) ? 2 : 1
275
+ end
276
+
277
+ def perform_move(player_number, from, to, captured)
278
+ if captured
279
+ captured.piece = nil
280
+ end
281
+ to.piece = from.piece
282
+ from.piece = nil
283
+ end
284
+ end
285
+ end
286
+
@@ -0,0 +1,71 @@
1
+ require 'just_xiangqi/pieces/jiang'
2
+ require 'just_xiangqi/pieces/ju'
3
+ require 'just_xiangqi/pieces/ma'
4
+ require 'just_xiangqi/pieces/pao'
5
+ require 'just_xiangqi/pieces/shi'
6
+ require 'just_xiangqi/pieces/xiang'
7
+ require 'just_xiangqi/pieces/zu'
8
+
9
+ module JustXiangqi
10
+
11
+ # = Piece Factory
12
+ #
13
+ # Generates pieces from a hash of arguments
14
+ class PieceFactory
15
+
16
+ # A mapping of type descriptions to classes.
17
+ CLASSES = {
18
+ 'jiang' => Jiang,
19
+ 'ju' => Ju,
20
+ 'ma' => Ma,
21
+ 'pao' => Pao,
22
+ 'shi' => Shi,
23
+ 'xiang' => Xiang,
24
+ 'zu' => Zu
25
+ }
26
+
27
+ # New objects can be instantiated by passing in a hash or the piece.
28
+ #
29
+ # @param [Hash,Piece] args
30
+ # the initial attributes of the piece, hash requires type key
31
+ #
32
+ # ==== Example:
33
+ # # Instantiates a new PieceFactory
34
+ # JustXiangqi::PieceFactory.new({
35
+ # type: 'zu',
36
+ # id: 1,
37
+ # player_number: 2
38
+ # })
39
+ def initialize(args)
40
+ @args = args
41
+ end
42
+
43
+ # Returns a piece based on the initial arguments.
44
+ #
45
+ # @return [Piece]
46
+ def build
47
+ case @args
48
+ when Hash
49
+ build_from_hash
50
+ when Piece
51
+ @args
52
+ when nil
53
+ nil
54
+ else
55
+ raise ArgumentError, "piece must be Hash or NilClass"
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def build_from_hash
62
+ klass = CLASSES[@args[:type]]
63
+ if klass
64
+ klass.new(**@args)
65
+ else
66
+ raise ArgumentError, "invalid piece type: #{@args[:type].to_s}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+
@@ -0,0 +1,31 @@
1
+ require 'just_xiangqi/pieces/piece'
2
+
3
+ module JustXiangqi
4
+
5
+ # = Jiang
6
+ #
7
+ # The piece that can move 1 space orthogonally within the palace.
8
+ class Jiang < Piece
9
+ PALACE_X_COORDINATES = [3, 4, 5]
10
+ PALACE_Y_COORDINATES = [0, 1, 2, 7, 8, 9]
11
+
12
+ # All the squares that the piece can move to and/or capture.
13
+ #
14
+ # @param [Square] square
15
+ # the origin square.
16
+ #
17
+ # @param [GameState] game_state
18
+ # the current game state.
19
+ #
20
+ # @return [SquareSet]
21
+ def destinations(square, game_state)
22
+ palace_squares(game_state).orthogonal(square).at_range(square, 1).unoccupied_or_occupied_by_opponent(player_number)
23
+ end
24
+
25
+ private
26
+
27
+ def palace_squares(game_state)
28
+ game_state.squares.where(x: PALACE_X_COORDINATES, y: PALACE_Y_COORDINATES)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ require 'just_xiangqi/pieces/piece'
2
+
3
+ module JustXiangqi
4
+ # = Ju
5
+ #
6
+ # The piece that can move orthogonally.
7
+ class Ju < Piece
8
+ # All the squares that the piece can move to and/or capture.
9
+ #
10
+ # @param [Square] square
11
+ # the origin square.
12
+ #
13
+ # @param [GameState] game_state
14
+ # the current game state.
15
+ def destinations(square, game_state)
16
+ game_state.squares.orthogonal(square).unoccupied_or_occupied_by_opponent(player_number).unblocked(square, game_state.squares)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ require 'just_xiangqi/pieces/piece'
2
+
3
+ module JustXiangqi
4
+ # = Ma
5
+ #
6
+ # The piece that can move in an l shape, but can be blocked by adjacent pieces.
7
+ class Ma < Piece
8
+ MOVEMENT_MAP = {
9
+ [0,-1] => [[-1,-2], [1,-2]],
10
+ [1,0] => [[2,-1], [2,1]],
11
+ [0,1] => [[-1,2], [1,2]],
12
+ [-1,0] => [[-2,-1], [-2,1]]
13
+ }
14
+ # All the squares that the piece can move to and/or capture.
15
+ #
16
+ # @param [Square] square
17
+ # the origin square.
18
+ #
19
+ # @param [GameState] game_state
20
+ # the current game state.
21
+ #
22
+ # @return [SquareSet]
23
+ def destinations(square, game_state)
24
+ _squares = MOVEMENT_MAP.map do |adjacent_map, destinations_map|
25
+ adjacent = find_square_by_displacement(square, game_state, adjacent_map)
26
+
27
+ if adjacent.nil? || adjacent.occupied?
28
+ nil
29
+ else
30
+ find_valid_destinations(square, game_state, destinations_map)
31
+ end
32
+ end.flatten.compact
33
+
34
+ SquareSet.new(squares: _squares)
35
+ end
36
+
37
+ private
38
+
39
+ def find_valid_destinations(square, game_state, destinations_map)
40
+ destinations_map.map do |destination_map|
41
+ find_valid_destination(square, game_state, destination_map)
42
+ end
43
+ end
44
+
45
+ def find_valid_destination(square, game_state, destination_map)
46
+ destination = find_square_by_displacement(square, game_state, destination_map)
47
+ if destination && (destination.unoccupied? || destination.occupied_by_opponent?(player_number))
48
+ destination
49
+ else
50
+ nil
51
+ end
52
+ end
53
+
54
+ def find_square_by_displacement(square, game_state, displacement)
55
+ x = square.x + displacement[0]
56
+ y = square.y + displacement[1]
57
+ game_state.squares.find_by_x_and_y(x, y)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,58 @@
1
+ require 'just_xiangqi/pieces/piece'
2
+
3
+ require 'board_game_grid'
4
+
5
+ module JustXiangqi
6
+ # = Ju
7
+ #
8
+ # The piece that can move orthogonally and captures by jumping over
9
+ class Pao < Piece
10
+ MOVEMENT_VECTORS = [
11
+ BoardGameGrid::Point.new(0, -1),
12
+ BoardGameGrid::Point.new(1, 0),
13
+ BoardGameGrid::Point.new(0, 1),
14
+ BoardGameGrid::Point.new(-1, 0)
15
+ ]
16
+
17
+ # All the squares that the piece can move to and/or capture.
18
+ #
19
+ # @param [Square] square
20
+ # the origin square.
21
+ #
22
+ # @param [GameState] game_state
23
+ # the current game state.
24
+ def destinations(square, game_state)
25
+ move_squares(square, game_state) + capture_squares(square, game_state)
26
+ end
27
+
28
+ private
29
+
30
+ def move_squares(square, game_state)
31
+ game_state.squares.orthogonal(square).unoccupied.unblocked(square, game_state.squares)
32
+ end
33
+
34
+ def capture_squares(square, game_state)
35
+ squares = []
36
+ MOVEMENT_VECTORS.each do |vector|
37
+ current_point = square.point + vector
38
+ current_square = game_state.squares.find_by_x_and_y(current_point.x, current_point.y)
39
+
40
+ # iterate through unoccupied square until end of board or occupied opponent square is reached
41
+ while !current_square.nil? && current_square.unoccupied?
42
+ current_point = current_square.point + vector
43
+ current_square = game_state.squares.find_by_x_and_y(current_point.x, current_point.y)
44
+ end
45
+
46
+ # find the next square after the occupied square
47
+ if current_square&.occupied_by_opponent?(player_number)
48
+ next_point = current_square.point + vector
49
+ next_square = game_state.squares.find_by_x_and_y(next_point.x, next_point.y)
50
+ # add the next square if it's unoccupied
51
+ squares.push(next_square) if !next_square.nil? && next_square.unoccupied?
52
+ end
53
+ end
54
+
55
+ SquareSet.new(squares: squares)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,14 @@
1
+ require 'board_game_grid'
2
+
3
+ module JustXiangqi
4
+
5
+ # = Piece
6
+ #
7
+ # A piece that can move on a board
8
+ class Piece < BoardGameGrid::Piece
9
+ def initialize(id: , player_number: , type: nil)
10
+ @id = id
11
+ @player_number = player_number
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ require 'just_xiangqi/pieces/piece'
2
+
3
+ module JustXiangqi
4
+
5
+ # = Shi
6
+ #
7
+ # The piece that can move 1 space diagonally within the palace.
8
+ class Shi < Piece
9
+ PALACE_X_COORDINATES = [3, 4, 5]
10
+ PALACE_Y_COORDINATES = [0, 1, 2, 7, 8, 9]
11
+
12
+ # All the squares that the piece can move to and/or capture.
13
+ #
14
+ # @param [Square] square
15
+ # the origin square.
16
+ #
17
+ # @param [GameState] game_state
18
+ # the current game state.
19
+ #
20
+ # @return [SquareSet]
21
+ def destinations(square, game_state)
22
+ palace_squares(game_state).diagonal(square).at_range(square, 1).unoccupied_or_occupied_by_opponent(player_number)
23
+ end
24
+
25
+ private
26
+
27
+ def palace_squares(game_state)
28
+ game_state.squares.where(x: PALACE_X_COORDINATES, y: PALACE_Y_COORDINATES)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ require 'just_xiangqi/pieces/piece'
2
+
3
+ module JustXiangqi
4
+
5
+ # = Xiang
6
+ #
7
+ # The piece that can move 2 spaces diagonally on its side of the river
8
+ class Xiang < Piece
9
+ # All the squares that the piece can move to and/or capture.
10
+ #
11
+ # @param [Square] square
12
+ # the origin square.
13
+ #
14
+ # @param [GameState] game_state
15
+ # the current game state.
16
+ #
17
+ # @return [SquareSet]
18
+ def destinations(square, game_state)
19
+ players_side_squares(game_state).diagonal(square).at_range(square, 2).unoccupied_or_occupied_by_opponent(player_number)
20
+ end
21
+
22
+ private
23
+
24
+ def players_side_squares(game_state)
25
+ condition = player_number == 2 ? -> (y) { y <= 4 } : -> (y) { y >= 5 }
26
+ game_state.squares.where(y: condition)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,43 @@
1
+ require 'just_xiangqi/pieces/piece'
2
+
3
+ module JustXiangqi
4
+ # = Zu
5
+ #
6
+ # The piece that can move one space forward and can also move one space horizontally when on the opposite side of the river.
7
+ class Zu < Piece
8
+ # All the squares that the piece can move to and/or capture.
9
+ #
10
+ # @param [Square] square
11
+ # the origin square.
12
+ #
13
+ # @param [GameState] game_state
14
+ # the current game state.
15
+ #
16
+ # @return [SquareSet]
17
+ def destinations(square, game_state)
18
+ same_file_forwards_and_or_same_rank(square, game_state).in_range(square, 1).unoccupied_or_occupied_by_opponent(player_number)
19
+ end
20
+
21
+ private
22
+
23
+ def same_file_forwards_and_or_same_rank(square, game_state)
24
+ if on_players_side(square)
25
+ same_file_forwards(square, game_state)
26
+ else
27
+ same_file_forwards(square, game_state) + same_rank(square, game_state)
28
+ end
29
+ end
30
+
31
+ def same_file_forwards(square, game_state)
32
+ game_state.squares.in_direction(square, forwards_direction).same_file(square)
33
+ end
34
+
35
+ def same_rank(square, game_state)
36
+ game_state.squares.same_rank(square)
37
+ end
38
+
39
+ def on_players_side(square)
40
+ (player_number == 2 && square.y <= 4) || (player_number == 1 && square.y >= 5)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ require 'just_xiangqi/piece_factory'
2
+ require 'board_game_grid'
3
+
4
+ module JustXiangqi
5
+
6
+ # = Square
7
+ #
8
+ # A Square on a shogi board.
9
+ class Square < BoardGameGrid::Square
10
+
11
+ # New object can be instantiated by passing in a hash with
12
+ #
13
+ # @param [String] id
14
+ # the unique identifier of the square.
15
+ #
16
+ # @param [Fixnum] x
17
+ # the x co-ordinate of the square.
18
+ #
19
+ # @param [Fixnum] y
20
+ # the y co-ordinate of the square.
21
+ #
22
+ # @option [Piece,Hash,NilClass] piece
23
+ # The piece on the square, can be a piece object or hash or nil.
24
+ #
25
+ # ==== Example:
26
+ # # Instantiates a new Square
27
+ # JustXiangqi::Square.new({
28
+ # id: '91',
29
+ # x: 0,
30
+ # y: 0,
31
+ # piece: { id: 1, player_number: 1, type: 'zu' }
32
+ # })
33
+ def initialize(id: , x: , y: , piece: nil)
34
+ @id = id
35
+ @x = x
36
+ @y = y
37
+ @piece = PieceFactory.new(piece).build
38
+ end
39
+ end
40
+ end
41
+