3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2008 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 # Split a moves line into an array of a move string.
29 # If it fails to parse the moves, it raises WrongMoves.
30 # @param moves a moves line. Ex. "+776FU-3334Fu"
31 # @return an array of a move string. Ex. ["+7776FU", "-3334FU"]
33 def Board.split_moves(moves)
36 rs = moves.gsub %r{[\+\-]\d{4}\w{2}} do |s|
40 raise WrongMoves, rs unless rs.empty?
45 def initialize(move_count=0)
46 @sente_hands = Array::new
47 @gote_hands = Array::new
48 @history = Hash::new(0)
49 @sente_history = Hash::new(0)
50 @gote_history = Hash::new(0)
51 @array = [[], [], [], [], [], [], [], [], [], []]
52 @move_count = move_count
53 @teban = nil # black => true, white => false
56 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
57 attr_reader :move_count
59 # Initial moves for a Buoy game. If it is an empty array, the game is
60 # normal with the initial setting; otherwise, the game is started after the
62 attr_reader :initial_moves
64 # See if self equals rhs, including a logical board position (i.e.
65 # not see object IDs) and sennichite stuff.
68 return @teban == rhs.teban &&
69 @move_count == rhs.move_count &&
71 @history == rhs.history &&
72 @sente_history == rhs.sente_history &&
73 @gote_history == rhs.gote_history &&
74 @initial_moves == rhs.initial_moves
78 return Marshal.load(Marshal.dump(self))
82 PieceKY::new(self, 1, 1, false)
83 PieceKE::new(self, 2, 1, false)
84 PieceGI::new(self, 3, 1, false)
85 PieceKI::new(self, 4, 1, false)
86 PieceOU::new(self, 5, 1, false)
87 PieceKI::new(self, 6, 1, false)
88 PieceGI::new(self, 7, 1, false)
89 PieceKE::new(self, 8, 1, false)
90 PieceKY::new(self, 9, 1, false)
91 PieceKA::new(self, 2, 2, false)
92 PieceHI::new(self, 8, 2, false)
94 PieceFU::new(self, i, 3, false)
97 PieceKY::new(self, 1, 9, true)
98 PieceKE::new(self, 2, 9, true)
99 PieceGI::new(self, 3, 9, true)
100 PieceKI::new(self, 4, 9, true)
101 PieceOU::new(self, 5, 9, true)
102 PieceKI::new(self, 6, 9, true)
103 PieceGI::new(self, 7, 9, true)
104 PieceKE::new(self, 8, 9, true)
105 PieceKY::new(self, 9, 9, true)
106 PieceKA::new(self, 8, 8, true)
107 PieceHI::new(self, 2, 8, true)
109 PieceFU::new(self, i, 7, true)
114 # Set up a board with the strs.
115 # Failing to parse the moves raises an StandardError.
116 # @param strs a board text
118 def set_from_str(strs)
119 strs.each_line do |str|
122 str.sub!(/^P(.)/, '')
125 while (str.length > 2)
126 str.sub!(/^(...?)/, '')
128 if (one =~ /^([\+\-])(..)/)
136 if ((x < 1) || (9 < x) || (y < 1) || (9 < y))
137 raise "bad position #{x} #{y}"
141 PieceFU::new(self, x, y, sente)
143 PieceKY::new(self, x, y, sente)
145 PieceKE::new(self, x, y, sente)
147 PieceGI::new(self, x, y, sente)
149 PieceKI::new(self, x, y, sente)
151 PieceOU::new(self, x, y, sente)
153 PieceKA::new(self, x, y, sente)
155 PieceHI::new(self, x, y, sente)
157 PieceFU::new(self, x, y, sente, true)
159 PieceKY::new(self, x, y, sente, true)
161 PieceKE::new(self, x, y, sente, true)
163 PieceGI::new(self, x, y, sente, true)
165 PieceKA::new(self, x, y, sente, true)
167 PieceHI::new(self, x, y, sente, true)
169 raise "unkown piece #{name}"
182 while (str.length > 3)
183 str.sub!(/^..(..)/, '')
187 PieceFU::new(self, 0, 0, sente)
189 PieceKY::new(self, 0, 0, sente)
191 PieceKE::new(self, 0, 0, sente)
193 PieceGI::new(self, 0, 0, sente)
195 PieceKI::new(self, 0, 0, sente)
197 PieceKA::new(self, 0, 0, sente)
199 PieceHI::new(self, 0, 0, sente)
201 raise "unkown piece #{name}"
209 raise "bad line: #{str}"
214 # Set up a board starting with a position after the moves.
215 # Failing to parse the moves raises an ArgumentError.
216 # @param moves an array of moves. ex. ["+7776FU", "-3334FU"]
218 def set_from_moves(moves)
220 return :normal if moves.empty?
223 rt = handle_one_move(move, @teban)
224 raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
226 @initial_moves = moves.dup
229 def have_piece?(hands, name)
230 piece = hands.find { |i|
236 # :illegal and :outori do not change this instance (self).
239 x0, x1, y0, y1, name, sente = move.x0, move.x1, move.y0, move.y1, move.name, move.sente
247 piece = have_piece?(hands, name)
248 return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
249 piece.move_to(x1, y1)
251 if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
254 if (@array[x1][y1] && @array[x1][y1].name == "OU")
258 # Change the state of this instance (self)
259 if (@array[x0][y0].name != name && !@array[x0][y0].promoted)
260 # the piece is promoting
261 @array[x0][y0].promoted = true
262 move.promotion = true
264 if (@array[x1][y1]) # capture
265 move.set_captured_piece(@array[x1][y1])
266 @array[x1][y1].sente = @array[x0][y0].sente
267 @array[x1][y1].move_to(0, 0)
268 hands.sort! {|a, b| # TODO refactor. Move to Piece class
272 @array[x0][y0].move_to(x1, y1)
275 @teban = @teban ? false : true
279 # Get back the previous move, which moved a name piece from [x0,y0] to
280 # [x1, y1] with or without promotion. If the move captured
281 # a piece, it is captured_piece, which is now in hand. The captured_piece
282 # was promoted or unpromoted.
291 piece = @array[move.x1][move.y1]
295 piece.move_to(move.x0, move.y0)
296 piece.promoted = false if piece.promoted && move.promotion
297 if move.captured_piece
298 move.captured_piece.move_to(move.x1, move.y1)
299 move.captured_piece.sente = move.sente ? false : true
300 move.captured_piece.promoted = true if move.captured_piece_promoted
305 @teban = @teban ? false : true
309 # def each_reserved_square
311 def look_for_ou(sente)
317 (@array[x][y].name == "OU") &&
318 (@array[x][y].sente == sente))
325 raise "can't find ou"
328 # See if sente is checked (i.e. loosing) or not.
329 # Note that the method name "checkmated?" is wrong. Instead, it should be
332 def checkmated?(sente)
333 ou = look_for_ou(sente)
339 (@array[x][y].sente != sente))
340 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
351 # See if sente's FU drop checkmates the opponent or not.
353 def uchifuzume?(sente)
354 rival_ou = look_for_ou(! sente) # rival's ou
355 if (sente) # rival is gote
356 if ((rival_ou.y != 9) &&
357 (@array[rival_ou.x][rival_ou.y + 1]) &&
358 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
359 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
361 fu_y = rival_ou.y + 1
366 if ((rival_ou.y != 1) &&
367 (@array[rival_ou.x][rival_ou.y - 1]) &&
368 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
369 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
371 fu_y = rival_ou.y - 1
377 ## case: rival_ou is moving
378 rival_ou.movable_grids.each do |(cand_x, cand_y)|
379 tmp_board = deep_copy
380 s = tmp_board.move_to(Move.new(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente))
381 raise "internal error" if (s != true)
382 if (! tmp_board.checkmated?(! sente)) # good move
387 ## case: rival is capturing fu
393 (@array[x][y].sente != sente) &&
394 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
397 if (@array[x][y].promoted)
398 names << @array[x][y].promoted_name
400 names << @array[x][y].name
401 if @array[x][y].promoted_name &&
402 @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
403 names << @array[x][y].promoted_name
407 tmp_board = deep_copy
408 s = tmp_board.move_to(Move.new(x, y, fu_x, fu_y, name, ! sente))
412 tmp_board.checkmated?(! sente) # result
415 all_illegal = names.find {|a| a != :illegal}
416 raise "internal error: legal move not found" if all_illegal == nil
417 r = names.find {|a| a == false} # good move
418 return false if r == false # found good move
427 # @[sente|gote]_history has at least one item while the player is checking the other or
429 def update_sennichite(player)
432 if checkmated?(!player)
434 @sente_history["dummy"] = 1 # flag to see Sente player is checking Gote player
436 @gote_history["dummy"] = 1 # flag to see Gote player is checking Sente player
440 @sente_history.clear # no more continuous check
442 @gote_history.clear # no more continuous check
445 if @sente_history.size > 0 # possible for Sente's or Gote's turn
446 @sente_history[str] += 1
448 if @gote_history.size > 0 # possible for Sente's or Gote's turn
449 @gote_history[str] += 1
453 # Deep-copy sennichite stuff, which will later be available to restore.
455 def dup_sennichite_stuff
456 return [@history.dup, @sente_history.dup, @gote_history.dup]
459 # Restore sennihite stuff.
461 def restore_sennichite_stuff(history, sente_history, gote_history)
462 @history, @sente_history, @gote_history = history, sente_history, gote_history
465 def oute_sennichite?(player)
466 return nil unless sennichite?
470 if (@sente_history[to_s] >= 4) # sente is checking gote
471 return :oute_sennichite_sente_lose
472 elsif (@gote_history[to_s] >= 3) # sente is escaping
473 return :oute_sennichite_gote_lose
475 return nil # Not oute_sennichite, but sennichite
479 if (@gote_history[to_s] >= 4) # gote is checking sente
480 return :oute_sennichite_gote_lose
481 elsif (@sente_history[to_s] >= 3) # gote is escaping
482 return :oute_sennichite_sente_lose
484 return nil # Not oute_sennichite, but sennichite
490 if (@history[to_s] >= 4) # already 3 times
496 def good_kachi?(sente)
497 if (checkmated?(sente))
498 puts "'NG: Checkmating." if $DEBUG
502 ou = look_for_ou(sente)
503 if (sente && (ou.y >= 4))
504 puts "'NG: Black's OU does not enter yet." if $DEBUG
507 if (! sente && (ou.y <= 6))
508 puts "'NG: White's OU does not enter yet." if $DEBUG
526 (@array[x][y].sente == sente) &&
527 (@array[x][y].point > 0))
528 point = point + @array[x][y].point
534 hands.each do |piece|
535 point = point + piece.point
539 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
544 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
549 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
554 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
558 # sente is nil only if tests in test_board run
565 # - :oute_sennichite_sente_lose
566 # - :oute_sennichite_gote_lose
570 # - (:outori will not be returned)
572 def handle_one_move(str, sente=nil)
573 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
580 elsif (str =~ /^%KACHI/)
581 raise ArgumentError, "sente is null", caller if sente == nil
582 if (good_kachi?(sente))
587 elsif (str =~ /^%TORYO/)
593 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
594 ((x0 != 0) || (y0 != 0)))
596 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
601 sente = true if sente == nil # deprecated
602 return :illegal unless sente == true # black player's move must be black
605 sente = false if sente == nil # deprecated
606 return :illegal unless sente == false # white player's move must be white
611 if ((x0 == 0) && (y0 == 0))
612 return :illegal if (! have_piece?(hands, name))
613 elsif (! @array[x0][y0])
614 return :illegal # no piece
615 elsif (@array[x0][y0].sente != sente)
616 return :illegal # this is not mine
617 elsif (@array[x0][y0].name != name)
618 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
622 if (@array[x1][y1] &&
623 (@array[x1][y1].sente == sente)) # can't capture mine
625 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
626 return :illegal # can't put on existing piece
629 move = Move.new(x0, y0, x1, y1, name, sente)
630 result = move_to(move)
631 if (result == :illegal)
635 if (checkmated?(sente))
637 return :oute_kaihimore
639 if ((x0 == 0) && (y0 == 0) && (name == "FU") && uchifuzume?(sente))
644 sennichite_stuff = dup_sennichite_stuff
645 update_sennichite(sente)
646 os_result = oute_sennichite?(sente)
647 if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
649 restore_sennichite_stuff(*sennichite_stuff)
654 restore_sennichite_stuff(*sennichite_stuff)
665 a.push(sprintf("P%d", y))
677 a.push(sprintf("\n"))
680 if (! sente_hands.empty?)
682 sente_hands.each do |p|
683 a.push("00" + p.name)
687 if (! gote_hands.empty?)
689 gote_hands.each do |p|
690 a.push("00" + p.name)
694 a.push("%s\n" % [@teban ? "+" : "-"])