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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +71 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/just_xiangqi.gemspec +29 -0
- data/lib/just_xiangqi/errors/error.rb +21 -0
- data/lib/just_xiangqi/errors/invalid_move_error.rb +23 -0
- data/lib/just_xiangqi/errors/moved_into_check_error.rb +23 -0
- data/lib/just_xiangqi/errors/no_legal_moves_error.rb +23 -0
- data/lib/just_xiangqi/errors/no_piece_error.rb +23 -0
- data/lib/just_xiangqi/errors/not_players_turn_error.rb +23 -0
- data/lib/just_xiangqi/errors/off_board_error.rb +23 -0
- data/lib/just_xiangqi/errors/piece_not_found_error.rb +23 -0
- data/lib/just_xiangqi/errors/square_occupied_error.rb +23 -0
- data/lib/just_xiangqi/game_state.rb +286 -0
- data/lib/just_xiangqi/piece_factory.rb +71 -0
- data/lib/just_xiangqi/pieces/jiang.rb +31 -0
- data/lib/just_xiangqi/pieces/ju.rb +19 -0
- data/lib/just_xiangqi/pieces/ma.rb +60 -0
- data/lib/just_xiangqi/pieces/pao.rb +58 -0
- data/lib/just_xiangqi/pieces/piece.rb +14 -0
- data/lib/just_xiangqi/pieces/shi.rb +31 -0
- data/lib/just_xiangqi/pieces/xiang.rb +29 -0
- data/lib/just_xiangqi/pieces/zu.rb +43 -0
- data/lib/just_xiangqi/square.rb +41 -0
- data/lib/just_xiangqi/square_set.rb +69 -0
- data/lib/just_xiangqi/version.rb +3 -0
- data/lib/just_xiangqi.rb +9 -0
- metadata +133 -0
@@ -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
|
+
|