3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ## GNU General Public License for more details.
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 require 'shogi_server/move'
22 module ShogiServer # for a namespace
24 class WrongMoves < ArgumentError; end
28 # Initial board setup.
29 # The string ends with '+', not a line break.
31 INITIAL_HIRATE_POSITION = (<<-EOF).chomp
32 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
33 P2 * -HI * * * * * -KA *
34 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
38 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
39 P8 * +KA * * * * * +HI *
40 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
44 # Split a moves line into an array of a move string.
45 # If it fails to parse the moves, it raises WrongMoves.
46 # @param moves a moves line. Ex. "+776FU-3334FU" or
47 # moves with times. Ex "+776FU,T2-3334FU,T5"
48 # @return an array of a move string. Ex. ["+7776FU", "-3334FU"] or
49 # an array of arrays. Ex. [["+7776FU","T2"], ["-3334FU", "T5"]]
51 def Board.split_moves(moves)
57 if moves[i,1] == "+" ||
60 if i == moves.size - 1
66 raise WrongMoves, a[0]
68 if a.size == 1 # "+7776FU"
71 unless /^T\d+/ =~ a[1]
72 raise WrongMoves, a[1]
87 def initialize(options={})
88 @sente_hands = Array::new
89 @gote_hands = Array::new
90 @history = Hash::new(0)
91 @sente_history = Hash::new(0)
92 @gote_history = Hash::new(0)
93 @array = [[], [], [], [], [], [], [], [], [], []]
95 @teban = nil # black => true, white => false
98 @ous = [nil, nil] # keep OU pieces of Sente and Gote
100 @max_moves = options[:max_moves] ||
101 ($options && $options["max-moves"]) ||
103 @least_time_per_move = options[:least_time_per_move] ||
104 ($options && $options["least-time-per-move"]) ||
105 Default_Least_Time_Per_Move
107 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
108 attr_reader :move_count
110 # Initial moves for a Buoy game. If it is an empty array, the game is
111 # normal with the initial setting; otherwise, the game is started after the
113 attr_reader :initial_moves
115 # A move parsed by handle_one_move. If the move is not :normal, the board
116 # position may or may not be rolled back.
120 # Max_Moves of the CSA protocol
121 attr_reader :max_moves
123 # Least_Time_Per_Move of the CSA protocol
124 attr_reader :least_time_per_move
126 # See if self equals rhs, including a logical board position (i.e.
127 # not see object IDs) and sennichite stuff.
130 return @teban == rhs.teban &&
131 @move_count == rhs.move_count &&
133 @history == rhs.history &&
134 @sente_history == rhs.sente_history &&
135 @gote_history == rhs.gote_history &&
136 @initial_moves == rhs.initial_moves
140 return Marshal.load(Marshal.dump(self))
144 PieceKY::new(self, 1, 1, false)
145 PieceKE::new(self, 2, 1, false)
146 PieceGI::new(self, 3, 1, false)
147 PieceKI::new(self, 4, 1, false)
148 PieceOU::new(self, 5, 1, false)
149 PieceKI::new(self, 6, 1, false)
150 PieceGI::new(self, 7, 1, false)
151 PieceKE::new(self, 8, 1, false)
152 PieceKY::new(self, 9, 1, false)
153 PieceKA::new(self, 2, 2, false)
154 PieceHI::new(self, 8, 2, false)
156 PieceFU::new(self, i, 3, false)
159 PieceKY::new(self, 1, 9, true)
160 PieceKE::new(self, 2, 9, true)
161 PieceGI::new(self, 3, 9, true)
162 PieceKI::new(self, 4, 9, true)
163 PieceOU::new(self, 5, 9, true)
164 PieceKI::new(self, 6, 9, true)
165 PieceGI::new(self, 7, 9, true)
166 PieceKE::new(self, 8, 9, true)
167 PieceKY::new(self, 9, 9, true)
168 PieceKA::new(self, 8, 8, true)
169 PieceHI::new(self, 2, 8, true)
171 PieceFU::new(self, i, 7, true)
177 # Piece#new will call back this method.
187 # Set up a board with the strs.
188 # Failing to parse the moves raises an StandardError.
189 # @param strs a board text
191 def set_from_str(strs)
192 strs.each_line do |str|
195 str.sub!(/^P(.)/, '')
198 while (str.length > 2)
199 str.sub!(/^(...?)/, '')
201 if (one =~ /^([\+\-])(..)/)
209 if ((x < 1) || (9 < x) || (y < 1) || (9 < y))
210 raise "bad position #{x} #{y}"
214 PieceFU::new(self, x, y, sente)
216 PieceKY::new(self, x, y, sente)
218 PieceKE::new(self, x, y, sente)
220 PieceGI::new(self, x, y, sente)
222 PieceKI::new(self, x, y, sente)
224 PieceOU::new(self, x, y, sente)
226 PieceKA::new(self, x, y, sente)
228 PieceHI::new(self, x, y, sente)
230 PieceFU::new(self, x, y, sente, true)
232 PieceKY::new(self, x, y, sente, true)
234 PieceKE::new(self, x, y, sente, true)
236 PieceGI::new(self, x, y, sente, true)
238 PieceKA::new(self, x, y, sente, true)
240 PieceHI::new(self, x, y, sente, true)
242 raise "unkown piece #{name}"
255 while (str.length > 3)
256 str.sub!(/^..(..)/, '')
260 PieceFU::new(self, 0, 0, sente)
262 PieceKY::new(self, 0, 0, sente)
264 PieceKE::new(self, 0, 0, sente)
266 PieceGI::new(self, 0, 0, sente)
268 PieceKI::new(self, 0, 0, sente)
270 PieceKA::new(self, 0, 0, sente)
272 PieceHI::new(self, 0, 0, sente)
274 raise "unkown piece #{name}"
282 raise "bad line: #{str}"
287 # Set up a board starting with a position after the moves.
288 # Failing to parse the moves raises an ArgumentError.
289 # @param moves an array of moves. ex. ["+7776FU", "-3334FU"] or
290 # an array of arrays. ex. [["+7776FU","T2"], ["-3334FU","T5"]]
292 def set_from_moves(moves)
294 return :normal if moves.empty?
300 rt = handle_one_move(move[0], @teban)
302 rt = handle_one_move(move, @teban)
304 raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
306 @initial_moves = moves.dup
309 def have_piece?(hands, name)
310 piece = hands.find { |i|
316 # :illegal and :outori do not change this instance (self).
319 x0, x1, y0, y1, name, sente = move.x0, move.x1, move.y0, move.y1, move.name, move.sente
327 piece = have_piece?(hands, name)
328 return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
329 piece.move_to(x1, y1)
331 if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
334 if (@array[x1][y1] && @array[x1][y1].name == "OU")
338 # Change the state of this instance (self)
339 if (@array[x0][y0].name != name && !@array[x0][y0].promoted)
340 # the piece is promoting
341 @array[x0][y0].promoted = true
342 move.promotion = true
344 if (@array[x1][y1]) # capture
345 move.set_captured_piece(@array[x1][y1])
346 @array[x1][y1].sente = @array[x0][y0].sente
347 @array[x1][y1].move_to(0, 0)
348 hands.sort! {|a, b| # TODO refactor. Move to Piece class
352 @array[x0][y0].move_to(x1, y1)
355 @teban = @teban ? false : true
359 # Get back the previous move, which moved a name piece from [x0,y0] to
360 # [x1, y1] with or without promotion. If the move captured
361 # a piece, it is captured_piece, which is now in hand. The captured_piece
362 # was promoted or unpromoted.
371 piece = @array[move.x1][move.y1]
375 piece.move_to(move.x0, move.y0)
376 piece.promoted = false if piece.promoted && move.promotion
377 if move.captured_piece
378 move.captured_piece.move_to(move.x1, move.y1)
379 move.captured_piece.sente = move.sente ? false : true
380 move.captured_piece.promoted = true if move.captured_piece_promoted
385 @teban = @teban ? false : true
389 # def each_reserved_square
391 def look_for_ou(sente)
399 # See if sente is checked (i.e. loosing) or not.
400 # Note that the method name "checkmated?" is wrong. Instead, it should be
403 def checkmated?(sente)
404 ou = look_for_ou(sente)
410 (@array[x][y].sente != sente))
411 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
422 # See if sente's FU drop checkmates the opponent or not.
424 def uchifuzume?(sente)
425 rival_ou = look_for_ou(! sente) # rival's ou
426 if (sente) # rival is gote
427 if ((rival_ou.y != 9) &&
428 (@array[rival_ou.x][rival_ou.y + 1]) &&
429 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
430 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
432 fu_y = rival_ou.y + 1
437 if ((rival_ou.y != 1) &&
438 (@array[rival_ou.x][rival_ou.y - 1]) &&
439 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
440 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
442 fu_y = rival_ou.y - 1
448 ## case: rival_ou is moving
449 rival_ou.movable_grids.each do |(cand_x, cand_y)|
450 tmp_board = deep_copy
451 s = tmp_board.move_to(Move.new(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente))
452 raise "internal error" if (s != true)
453 if (! tmp_board.checkmated?(! sente)) # good move
458 ## case: rival is capturing fu
464 (@array[x][y].sente != sente) &&
465 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
468 if (@array[x][y].promoted)
469 names << @array[x][y].promoted_name
471 names << @array[x][y].name
472 if @array[x][y].promoted_name &&
473 @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
474 names << @array[x][y].promoted_name
478 tmp_board = deep_copy
479 s = tmp_board.move_to(Move.new(x, y, fu_x, fu_y, name, ! sente))
483 tmp_board.checkmated?(! sente) # result
486 all_illegal = names.find {|a| a != :illegal}
487 raise "internal error: legal move not found" if all_illegal == nil
488 r = names.find {|a| a == false} # good move
489 return false if r == false # found good move
498 # @[sente|gote]_history has at least one item while the player is checking the other or
500 def update_sennichite(player)
503 if checkmated?(!player)
505 @sente_history["dummy"] = 1 # flag to see Sente player is checking Gote player
507 @gote_history["dummy"] = 1 # flag to see Gote player is checking Sente player
511 @sente_history.clear # no more continuous check
513 @gote_history.clear # no more continuous check
516 if @sente_history.size > 0 # possible for Sente's or Gote's turn
517 @sente_history[str] += 1
519 if @gote_history.size > 0 # possible for Sente's or Gote's turn
520 @gote_history[str] += 1
524 # Deep-copy sennichite stuff, which will later be available to restore.
526 def dup_sennichite_stuff
527 return [@history.dup, @sente_history.dup, @gote_history.dup]
530 # Restore sennihite stuff.
532 def restore_sennichite_stuff(history, sente_history, gote_history)
533 @history, @sente_history, @gote_history = history, sente_history, gote_history
536 def oute_sennichite?(player)
537 return nil unless sennichite?
541 if (@sente_history[to_s] >= 4) # sente is checking gote
542 return :oute_sennichite_sente_lose
543 elsif (@gote_history[to_s] >= 3) # sente is escaping
544 return :oute_sennichite_gote_lose
546 return nil # Not oute_sennichite, but sennichite
550 if (@gote_history[to_s] >= 4) # gote is checking sente
551 return :oute_sennichite_gote_lose
552 elsif (@sente_history[to_s] >= 3) # gote is escaping
553 return :oute_sennichite_sente_lose
555 return nil # Not oute_sennichite, but sennichite
561 if (@history[to_s] >= 4) # already 3 times
567 def good_kachi?(sente)
568 if (checkmated?(sente))
569 puts "'NG: Checkmating." if $DEBUG
573 ou = look_for_ou(sente)
574 if (sente && (ou.y >= 4))
575 puts "'NG: Black's OU does not enter yet." if $DEBUG
578 if (! sente && (ou.y <= 6))
579 puts "'NG: White's OU does not enter yet." if $DEBUG
597 (@array[x][y].sente == sente) &&
598 (@array[x][y].point > 0))
599 point = point + @array[x][y].point
605 hands.each do |piece|
606 point = point + piece.point
610 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
615 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
620 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
625 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
629 # sente is nil only if tests in test_board run
636 # - :oute_sennichite_sente_lose
637 # - :oute_sennichite_gote_lose
641 # - (:outori will not be returned)
644 def handle_one_move(str, sente=nil)
645 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
652 elsif (str =~ /^%KACHI/)
653 raise ArgumentError, "sente is null", caller if sente == nil
654 if (good_kachi?(sente))
659 elsif (str =~ /^%TORYO/)
665 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
666 ((x0 != 0) || (y0 != 0)))
668 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
673 sente = true if sente == nil # deprecated
674 return :illegal unless sente == true # black player's move must be black
677 sente = false if sente == nil # deprecated
678 return :illegal unless sente == false # white player's move must be white
683 if ((x0 == 0) && (y0 == 0))
684 return :illegal if (! have_piece?(hands, name))
685 elsif (! @array[x0][y0])
686 return :illegal # no piece
687 elsif (@array[x0][y0].sente != sente)
688 return :illegal # this is not mine
689 elsif (@array[x0][y0].name != name)
690 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
694 if (@array[x1][y1] &&
695 (@array[x1][y1].sente == sente)) # can't capture mine
697 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
698 return :illegal # can't put on existing piece
701 @move = Move.new(x0, y0, x1, y1, name, sente)
702 result = move_to(@move)
703 if (result == :illegal)
707 if (checkmated?(sente))
709 return :oute_kaihimore
711 if ((x0 == 0) && (y0 == 0) && (name == "FU") && uchifuzume?(sente))
716 sennichite_stuff = dup_sennichite_stuff
717 update_sennichite(sente)
718 os_result = oute_sennichite?(sente)
719 if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
721 restore_sennichite_stuff(*sennichite_stuff)
726 restore_sennichite_stuff(*sennichite_stuff)
730 # New rule that CSA introduced in November 2014.
731 # If a game with 256 plies does not end, make the game a draw.
732 # When running test cases $options might be nil.
733 if @max_moves > 0 && @move_count >= @max_moves
740 # Return a CSA-styled string notation of the current position.
746 a.push(sprintf("P%d", y))
758 a.push(sprintf("\n"))
761 if (! sente_hands.empty?)
763 sente_hands.each do |p|
764 a.push("00" + p.name)
768 if (! gote_hands.empty?)
770 gote_hands.each do |p|
771 a.push("00" + p.name)
775 a.push("%s\n" % [@teban ? "+" : "-"])
779 # Return a CSA-styled string notation of the initial position.
782 tmp_board = self.class.new
784 return tmp_board.to_s