just_xiangqi 0.1.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.
@@ -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
+