just_shogi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/CODE_OF_CONDUCT.md +74 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +24 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +80 -0
  8. data/Rakefile +10 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/just_shogi.gemspec +29 -0
  12. data/lib/just_shogi.rb +8 -0
  13. data/lib/just_shogi/errors/causes_check_error.rb +22 -0
  14. data/lib/just_shogi/errors/dropped_into_check_error.rb +22 -0
  15. data/lib/just_shogi/errors/error.rb +20 -0
  16. data/lib/just_shogi/errors/invalid_move_error.rb +22 -0
  17. data/lib/just_shogi/errors/invalid_promotion_error.rb +22 -0
  18. data/lib/just_shogi/errors/moved_into_check_error.rb +22 -0
  19. data/lib/just_shogi/errors/no_piece_error.rb +22 -0
  20. data/lib/just_shogi/errors/not_players_turn_error.rb +22 -0
  21. data/lib/just_shogi/errors/off_board_error.rb +22 -0
  22. data/lib/just_shogi/errors/piece_not_found_error.rb +22 -0
  23. data/lib/just_shogi/errors/square_occupied_error.rb +22 -0
  24. data/lib/just_shogi/game_state.rb +355 -0
  25. data/lib/just_shogi/hand.rb +48 -0
  26. data/lib/just_shogi/piece_factory.rb +87 -0
  27. data/lib/just_shogi/pieces/fuhyou.rb +23 -0
  28. data/lib/just_shogi/pieces/ginshou.rb +23 -0
  29. data/lib/just_shogi/pieces/gyokushou.rb +9 -0
  30. data/lib/just_shogi/pieces/hisha.rb +23 -0
  31. data/lib/just_shogi/pieces/kakugyou.rb +23 -0
  32. data/lib/just_shogi/pieces/keima.rb +23 -0
  33. data/lib/just_shogi/pieces/kin_base.rb +23 -0
  34. data/lib/just_shogi/pieces/kinshou.rb +9 -0
  35. data/lib/just_shogi/pieces/kyousha.rb +23 -0
  36. data/lib/just_shogi/pieces/narigin.rb +9 -0
  37. data/lib/just_shogi/pieces/narikei.rb +9 -0
  38. data/lib/just_shogi/pieces/narikyou.rb +9 -0
  39. data/lib/just_shogi/pieces/ou_base.rb +70 -0
  40. data/lib/just_shogi/pieces/oushou.rb +9 -0
  41. data/lib/just_shogi/pieces/piece.rb +18 -0
  42. data/lib/just_shogi/pieces/ryuuma.rb +23 -0
  43. data/lib/just_shogi/pieces/ryuuou.rb +23 -0
  44. data/lib/just_shogi/pieces/tokin.rb +9 -0
  45. data/lib/just_shogi/promotion_factory.rb +71 -0
  46. data/lib/just_shogi/square.rb +51 -0
  47. data/lib/just_shogi/square_set.rb +64 -0
  48. data/lib/just_shogi/version.rb +3 -0
  49. metadata +147 -0
@@ -0,0 +1,22 @@
1
+ require 'just_shogi/errors/error'
2
+
3
+ module JustShogi
4
+
5
+ # = NoPieceError
6
+ #
7
+ # A no piece error with a message
8
+ class NoPieceError < Error
9
+
10
+ # New no piece errors can be instantiated with
11
+ #
12
+ # @option [String] message
13
+ # the message to display.
14
+ #
15
+ # ==== Example:
16
+ # # Instantiates a new NoPieceError
17
+ # JustShogi::NoPieceError.new("Custom Message")
18
+ def initialize(message="There is no piece to move.")
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'just_shogi/errors/error'
2
+
3
+ module JustShogi
4
+
5
+ # = NotPlayersTurnError
6
+ #
7
+ # A not players turn error with a message
8
+ class NotPlayersTurnError < Error
9
+
10
+ # New not players turn errors can be instantiated with
11
+ #
12
+ # @option [String] message
13
+ # the message to display.
14
+ #
15
+ # ==== Example:
16
+ # # Intantiates a new NotPlayersTurnError
17
+ # JustShogi::NotPlayersTurnError.new("Custom Message")
18
+ def initialize(message="It is not the player's turn yet.")
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'just_shogi/errors/error'
2
+
3
+ module JustShogi
4
+
5
+ # = OffBoardError
6
+ #
7
+ # An off board error with a message
8
+ class OffBoardError < Error
9
+
10
+ # New off board errors can be instantiated with
11
+ #
12
+ # @option [String] message
13
+ # the message to display.
14
+ #
15
+ # ==== Example:
16
+ # # Instantiates a new OffBoardError
17
+ # JustShogi::OffBoardError.new("Custom Message")
18
+ def initialize(message="Cannot move off board.")
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'just_shogi/errors/error'
2
+
3
+ module JustShogi
4
+
5
+ # = PieceNotFoundError
6
+ #
7
+ # An piece not found error with a message
8
+ class PieceNotFoundError < Error
9
+
10
+ # New piece not found errors can be instantiated with
11
+ #
12
+ # @option [String] message
13
+ # the message to display.
14
+ #
15
+ # ==== Example:
16
+ # # Instantiates a new PieceNotFoundError
17
+ # JustShogi::PieceNotFoundError.new("Custom Message")
18
+ def initialize(message="The piece could not be found.")
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'just_shogi/errors/error'
2
+
3
+ module JustShogi
4
+
5
+ # = SquareOccupiedError
6
+ #
7
+ # A square occupied error with a message
8
+ class SquareOccupiedError < Error
9
+
10
+ # New invalid promotion errors can be instantiated with
11
+ #
12
+ # @option [String] message
13
+ # the message to display.
14
+ #
15
+ # ==== Example:
16
+ # # Instantiates a new SquareOccupiedError
17
+ # JustShogi::SquareOccupiedError.new("Custom Message")
18
+ def initialize(message="Square is already occupied.")
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,355 @@
1
+ require 'just_shogi/errors/no_piece_error'
2
+ require 'just_shogi/errors/not_players_turn_error'
3
+ require 'just_shogi/errors/off_board_error'
4
+ require 'just_shogi/errors/invalid_move_error'
5
+ require 'just_shogi/errors/moved_into_check_error'
6
+ require 'just_shogi/errors/invalid_promotion_error'
7
+ require 'just_shogi/errors/piece_not_found_error'
8
+ require 'just_shogi/errors/square_occupied_error'
9
+ require 'just_shogi/errors/dropped_into_check_error'
10
+ require 'just_shogi/square_set'
11
+ require 'just_shogi/hand'
12
+ require 'just_shogi/promotion_factory'
13
+
14
+ module JustShogi
15
+
16
+ # = Game State
17
+ #
18
+ # Represents a game of Shogi in progress.
19
+ class GameState
20
+ def initialize(current_player_number: , squares: [], hands: [])
21
+ @current_player_number = current_player_number
22
+ @squares = if squares.is_a?(SquareSet)
23
+ squares
24
+ else
25
+ SquareSet.new(squares: squares)
26
+ end
27
+ @hands = if hands.is_a?(Array)
28
+ if hands.all? { |hand| hand.is_a?(Hand) }
29
+ hands
30
+ elsif hands.all? { |hand| hand.is_a?(Hash) }
31
+ hands.map { |hand| Hand.new(**hand) }
32
+ else
33
+ raise ArgumentError, "hands must all be the same class"
34
+ end
35
+ else
36
+ raise ArgumentError, "hands must be an array"
37
+ end
38
+ end
39
+
40
+ attr_reader :current_player_number, :squares, :hands, :errors
41
+
42
+ # Instantiates a new GameState object in the starting position.
43
+ #
44
+ # @return [GameState]
45
+ def self.default
46
+ new(
47
+ current_player_number: 1,
48
+ squares: [
49
+ { id: '91', x: 0, y: 0, piece: { id: 1, player_number: 2, type: 'kyousha' } },
50
+ { id: '81', x: 1, y: 0, piece: { id: 2, player_number: 2, type: 'keima' } },
51
+ { id: '71', x: 2, y: 0, piece: { id: 3, player_number: 2, type: 'ginshou' } },
52
+ { id: '61', x: 3, y: 0, piece: { id: 4, player_number: 2, type: 'kinshou' } },
53
+ { id: '51', x: 4, y: 0, piece: { id: 5, player_number: 2, type: 'oushou' } },
54
+ { id: '41', x: 5, y: 0, piece: { id: 6, player_number: 2, type: 'kinshou' } },
55
+ { id: '31', x: 6, y: 0, piece: { id: 7, player_number: 2, type: 'ginshou' } },
56
+ { id: '21', x: 7, y: 0, piece: { id: 8, player_number: 2, type: 'keima' } },
57
+ { id: '11', x: 8, y: 0, piece: { id: 9, player_number: 2, type: 'kyousha' } },
58
+
59
+ { id: '92', x: 0, y: 1, piece: nil },
60
+ { id: '82', x: 1, y: 1, piece: { id: 10, player_number: 2, type: 'hisha' } },
61
+ { id: '72', x: 2, y: 1, piece: nil },
62
+ { id: '62', x: 3, y: 1, piece: nil },
63
+ { id: '52', x: 4, y: 1, piece: nil },
64
+ { id: '42', x: 5, y: 1, piece: nil },
65
+ { id: '32', x: 6, y: 1, piece: nil },
66
+ { id: '22', x: 7, y: 1, piece: { id: 11, player_number: 2, type: 'kakugyou' } },
67
+ { id: '12', x: 8, y: 1, piece: nil },
68
+
69
+ { id: '93', x: 0, y: 2, piece: { id: 12, player_number: 2, type: 'fuhyou' } },
70
+ { id: '83', x: 1, y: 2, piece: { id: 13, player_number: 2, type: 'fuhyou' } },
71
+ { id: '73', x: 2, y: 2, piece: { id: 14, player_number: 2, type: 'fuhyou' } },
72
+ { id: '63', x: 3, y: 2, piece: { id: 15, player_number: 2, type: 'fuhyou' } },
73
+ { id: '53', x: 4, y: 2, piece: { id: 16, player_number: 2, type: 'fuhyou' } },
74
+ { id: '43', x: 5, y: 2, piece: { id: 17, player_number: 2, type: 'fuhyou' } },
75
+ { id: '33', x: 6, y: 2, piece: { id: 18, player_number: 2, type: 'fuhyou' } },
76
+ { id: '23', x: 7, y: 2, piece: { id: 19, player_number: 2, type: 'fuhyou' } },
77
+ { id: '13', x: 8, y: 2, piece: { id: 20, player_number: 2, type: 'fuhyou' } },
78
+
79
+ { id: '94', x: 0, y: 3, piece: nil },
80
+ { id: '84', x: 1, y: 3, piece: nil },
81
+ { id: '74', x: 2, y: 3, piece: nil },
82
+ { id: '64', x: 3, y: 3, piece: nil },
83
+ { id: '54', x: 4, y: 3, piece: nil },
84
+ { id: '44', x: 5, y: 3, piece: nil },
85
+ { id: '34', x: 6, y: 3, piece: nil },
86
+ { id: '24', x: 7, y: 3, piece: nil },
87
+ { id: '14', x: 8, y: 3, piece: nil },
88
+
89
+ { id: '95', x: 0, y: 4, piece: nil },
90
+ { id: '85', x: 1, y: 4, piece: nil },
91
+ { id: '75', x: 2, y: 4, piece: nil },
92
+ { id: '65', x: 3, y: 4, piece: nil },
93
+ { id: '55', x: 4, y: 4, piece: nil },
94
+ { id: '45', x: 5, y: 4, piece: nil },
95
+ { id: '35', x: 6, y: 4, piece: nil },
96
+ { id: '25', x: 7, y: 4, piece: nil },
97
+ { id: '15', x: 8, y: 4, piece: nil },
98
+
99
+ { id: '96', x: 0, y: 5, piece: nil },
100
+ { id: '86', x: 1, y: 5, piece: nil },
101
+ { id: '76', x: 2, y: 5, piece: nil },
102
+ { id: '66', x: 3, y: 5, piece: nil },
103
+ { id: '56', x: 4, y: 5, piece: nil },
104
+ { id: '46', x: 5, y: 5, piece: nil },
105
+ { id: '36', x: 6, y: 5, piece: nil },
106
+ { id: '26', x: 7, y: 5, piece: nil },
107
+ { id: '16', x: 8, y: 5, piece: nil },
108
+
109
+ { id: '97', x: 0, y: 6, piece: { id: 21, player_number: 1, type: 'fuhyou' } },
110
+ { id: '87', x: 1, y: 6, piece: { id: 22, player_number: 1, type: 'fuhyou' } },
111
+ { id: '77', x: 2, y: 6, piece: { id: 23, player_number: 1, type: 'fuhyou' } },
112
+ { id: '67', x: 3, y: 6, piece: { id: 24, player_number: 1, type: 'fuhyou' } },
113
+ { id: '57', x: 4, y: 6, piece: { id: 25, player_number: 1, type: 'fuhyou' } },
114
+ { id: '47', x: 5, y: 6, piece: { id: 26, player_number: 1, type: 'fuhyou' } },
115
+ { id: '37', x: 6, y: 6, piece: { id: 27, player_number: 1, type: 'fuhyou' } },
116
+ { id: '27', x: 7, y: 6, piece: { id: 28, player_number: 1, type: 'fuhyou' } },
117
+ { id: '17', x: 8, y: 6, piece: { id: 29, player_number: 1, type: 'fuhyou' } },
118
+
119
+ { id: '98', x: 0, y: 7, piece: nil },
120
+ { id: '88', x: 1, y: 7, piece: { id: 30, player_number: 1, type: 'kakugyou' } },
121
+ { id: '78', x: 2, y: 7, piece: nil },
122
+ { id: '68', x: 3, y: 7, piece: nil },
123
+ { id: '58', x: 4, y: 7, piece: nil },
124
+ { id: '48', x: 5, y: 7, piece: nil },
125
+ { id: '38', x: 6, y: 7, piece: nil },
126
+ { id: '28', x: 7, y: 7, piece: { id: 31, player_number: 1, type: 'hisha' } },
127
+ { id: '18', x: 8, y: 7, piece: nil },
128
+
129
+ { id: '99', x: 0, y: 8, piece: { id: 32, player_number: 1, type: 'kyousha' } },
130
+ { id: '89', x: 1, y: 8, piece: { id: 33, player_number: 1, type: 'keima' } },
131
+ { id: '79', x: 2, y: 8, piece: { id: 34, player_number: 1, type: 'ginshou' } },
132
+ { id: '69', x: 3, y: 8, piece: { id: 35, player_number: 1, type: 'kinshou' } },
133
+ { id: '59', x: 4, y: 8, piece: { id: 36, player_number: 1, type: 'gyokushou' } },
134
+ { id: '49', x: 5, y: 8, piece: { id: 37, player_number: 1, type: 'kinshou' } },
135
+ { id: '39', x: 6, y: 8, piece: { id: 38, player_number: 1, type: 'ginshou' } },
136
+ { id: '29', x: 7, y: 8, piece: { id: 39, player_number: 1, type: 'keima' } },
137
+ { id: '19', x: 8, y: 8, piece: { id: 40, player_number: 1, type: 'kyousha' } }
138
+ ],
139
+ hands: [
140
+ { player_number: 1, pieces: [] },
141
+ { player_number: 2, pieces: [] },
142
+ ]
143
+ )
144
+ end
145
+
146
+ # serializes the game state as ahash
147
+ #
148
+ # @return [Hash]
149
+ def as_json
150
+ {
151
+ current_player_number: current_player_number,
152
+ squares: squares.as_json,
153
+ hands: hands.map(&:as_json)
154
+ }
155
+ end
156
+
157
+ # deep clone of the game state
158
+ #
159
+ # @return [GameState]
160
+ def clone
161
+ self.class.new(**as_json)
162
+ end
163
+
164
+ # Moves a piece owned by the player, from one square, to another.
165
+ #
166
+ # It has an option to promote the moving piece.
167
+ # It moves the piece and returns true if the move is valid and it's the player's turn.
168
+ # It returns false otherwise.
169
+ #
170
+ # ==== Example:
171
+ # # Moves a piece from a square to perform a move
172
+ # game_state.move(1, '77', '78')
173
+ #
174
+ # @param [Fixnum] player_number
175
+ # the player number, 1 or 2.
176
+ #
177
+ # @param [String] from_id
178
+ # the id of the from square
179
+ #
180
+ # @param [String] to_id
181
+ # the id of the to square
182
+ #
183
+ # @param [boolean] promote
184
+ #
185
+ # @return [Boolean]
186
+ def move(player_number, from_id, to_id, promote = false)
187
+ @errors = []
188
+
189
+ from = squares.find_by_id(from_id)
190
+ to = squares.find_by_id(to_id)
191
+
192
+ if current_player_number != player_number
193
+ @errors.push JustShogi::NotPlayersTurnError.new
194
+ elsif from.unoccupied?
195
+ @errors.push JustShogi::NoPieceError.new
196
+ elsif to.nil?
197
+ @errors.push JustShogi::OffBoardError.new
198
+ elsif promote && !promotable?(player_number, from, to)
199
+ @errors.push JustShogi::InvalidPromotionError.new
200
+ elsif from.piece.can_move?(from, to, self)
201
+
202
+ duplicate = self.clone
203
+ duplicate.perform_complete_move(player_number, from_id, to_id, promote)
204
+
205
+ if duplicate.in_check?(current_player_number)
206
+ @errors.push JustShogi::MovedIntoCheckError.new
207
+ else
208
+ perform_complete_move(player_number, from_id, to_id, promote)
209
+ end
210
+ else
211
+ @errors.push JustShogi::InvalidMoveError.new
212
+ end
213
+
214
+ @errors.empty?
215
+ end
216
+
217
+ def drop(player_number, piece_id, square_id)
218
+ @errors = []
219
+
220
+ piece = hand_for_player(player_number).find_piece_by_id(piece_id)
221
+ square = squares.find_by_id(square_id)
222
+
223
+ if current_player_number != player_number
224
+ @errors.push JustShogi::NotPlayersTurnError.new
225
+ elsif piece.nil?
226
+ @errors.push JustShogi::PieceNotFoundError.new
227
+ elsif square.nil?
228
+ @errors.push JustShogi::OffBoardError.new
229
+ elsif square.occupied?
230
+ @errors.push JustShogi::SquareOccupiedError.new
231
+ else
232
+ duplicate = self.clone
233
+ duplicate.perform_complete_drop(player_number, piece_id, square_id)
234
+
235
+ if duplicate.in_check?(opposing_player_number)
236
+ @errors.push JustShogi::DroppedIntoCheckError.new
237
+ else
238
+ perform_complete_drop(player_number, piece_id, square_id)
239
+ end
240
+ end
241
+
242
+ @errors.empty?
243
+ end
244
+
245
+ # The player number of the winner. It returns nil if there is no winner.
246
+ #
247
+ # @return [Fixnum,NilClass]
248
+ def winner
249
+ case
250
+ when in_checkmate?(1)
251
+ 2
252
+ when in_checkmate?(2)
253
+ 1
254
+ else
255
+ nil
256
+ end
257
+ end
258
+
259
+ # Moves a piece owned by the player, from one square, to another, with the option to promote.
260
+ #
261
+ # It moves the piece and returns true if the move is valid and it's the player's turn.
262
+ # It returns false otherwise.
263
+ # It promotes if possible and specified.
264
+ #
265
+ def perform_complete_move(player_number, from_id, to_id, promote = false)
266
+ from = squares.find_by_id(from_id)
267
+ to = squares.find_by_id(to_id)
268
+
269
+ captured = to.occupied? ? to : nil
270
+
271
+ @last_change = { type: 'move', data: { player_number: player_number, from: from_id, to: to_id } }
272
+
273
+ perform_move(player_number, from, to, captured)
274
+
275
+ promote_piece(to) if promote
276
+ pass_turn
277
+ end
278
+
279
+ def perform_complete_drop(player_number, piece_id, square_id)
280
+ hand = hand_for_player(player_number)
281
+ square = squares.find_by_id(square_id)
282
+
283
+ @last_change = { type: 'drop', data: { player_number: player_number, piece: piece_id, square: square_id } }
284
+
285
+ piece = hand.pop_piece(piece_id)
286
+ square.piece = piece
287
+
288
+ pass_turn
289
+ end
290
+
291
+ def in_check?(player_number)
292
+ ou_square = squares.find_ou_for_player(player_number)
293
+ threatened_by = squares.threatened_by(opposing_player_number(player_number), self)
294
+ threatened_by.include?(ou_square)
295
+ end
296
+
297
+ def in_checkmate?(player_number)
298
+ (in_check?(player_number) || non_ou_pieces_cannot_move?(player_number)) && ou_cannot_move?(player_number)
299
+ end
300
+
301
+ private
302
+
303
+ def non_ou_pieces_cannot_move?(player_number)
304
+ squares.occupied_by_player(player_number).excluding_piece(JustShogi::OuBase).all? { |s| s.piece.destinations(s, self).empty? }
305
+ end
306
+
307
+ def ou_cannot_move?(player_number)
308
+ ou_square = squares.find_ou_for_player(player_number)
309
+ destinations = ou_square.piece.destinations(ou_square, self)
310
+ destinations.all? do |destination|
311
+ duplicate = self.clone
312
+ duplicate.perform_complete_move(player_number, ou_square.id, destination.id)
313
+ duplicate.in_check?(player_number)
314
+ end
315
+ end
316
+
317
+ def pass_turn
318
+ @current_player_number = opposing_player_number
319
+ end
320
+
321
+ def opposing_player_number(player_number = nil)
322
+ if player_number
323
+ other_player_number(player_number)
324
+ else
325
+ other_player_number(@current_player_number)
326
+ end
327
+ end
328
+
329
+ def other_player_number(player_number)
330
+ (player_number == 1) ? 2 : 1
331
+ end
332
+
333
+ def hand_for_player(player_number)
334
+ hands.find { |h| h.player_number == player_number }
335
+ end
336
+
337
+ def perform_move(player_number, from, to, captured)
338
+ if captured
339
+ hand_for_player(player_number).push_piece(to.piece)
340
+ captured.piece = nil
341
+ end
342
+ to.piece = from.piece
343
+ from.piece = nil
344
+ end
345
+
346
+ def promotable?(player_number, from, to)
347
+ PromotionFactory.new(from.piece).promotable? && to.promotion_zone(player_number)
348
+ end
349
+
350
+ def promote_piece(square)
351
+ new_piece = PromotionFactory.new(square.piece).promote
352
+ square.piece = new_piece
353
+ end
354
+ end
355
+ end