OSDN Git Service

Fix test failures related to MAX_MOVES and least time per move
[shogi-server/shogi-server.git] / shogi_server / board.rb
index efc1e36..bcf8887 100644 (file)
@@ -1,7 +1,7 @@
 ## $Id$
 
 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
-## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
+## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
 ##
 ## This program is free software; you can redistribute it and/or modify
 ## it under the terms of the GNU General Public License as published by
 ## along with this program; if not, write to the Free Software
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+require 'shogi_server/move'
+
 module ShogiServer # for a namespace
 
+class WrongMoves < ArgumentError; end
+
 class Board
+  
+  # Initial board setup. 
+  # The string ends with '+', not a line break.
+  #
+  INITIAL_HIRATE_POSITION = (<<-EOF).chomp
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI *  *  *  *  * -KA * 
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 *  *  *  *  *  *  *  *  * 
+P5 *  *  *  *  *  *  *  *  * 
+P6 *  *  *  *  *  *  *  *  * 
+P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
+P8 * +KA *  *  *  *  * +HI * 
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+EOF
+
+  # Split a moves line into an array of a move string.
+  # If it fails to parse the moves, it raises WrongMoves.
+  # @param moves a moves line. Ex. "+776FU-3334FU" or
+  #              moves with times. Ex "+776FU,T2-3334FU,T5"
+  # @return an array of a move string. Ex. ["+7776FU", "-3334FU"] or
+  #         an array of arrays. Ex. [["+7776FU","T2"], ["-3334FU", "T5"]]
+  #
+  def Board.split_moves(moves)
+    ret = []
+
+    i=0
+    tmp = ""
+    while i<moves.size
+      if moves[i,1] == "+" ||
+         moves[i,1] == "-" ||
+         i == moves.size - 1
+        if i == moves.size - 1
+          tmp << moves[i,1]
+        end
+        unless tmp.empty?
+          a = tmp.split(",")
+          if a[0].size != 7
+            raise WrongMoves, a[0]
+          end
+          if a.size == 1 # "+7776FU"
+            ret << a[0]
+          else           # "+7776FU,T2"
+            unless /^T\d+/ =~ a[1] 
+              raise WrongMoves, a[1]
+            end
+            ret << a
+          end
+          tmp = ""
+        end
+      end
+      tmp << moves[i,1]
+      i += 1
+    end
+
+    return ret
+  end
+
+
   def initialize(move_count=0)
     @sente_hands = Array::new
     @gote_hands  = Array::new
@@ -29,25 +93,38 @@ class Board
     @array = [[], [], [], [], [], [], [], [], [], []]
     @move_count = move_count
     @teban = nil # black => true, white => false
+    @initial_moves = []
+    @move = nil
+    @ous = [nil, nil] # keep OU pieces of Sente and Gote
   end
   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
   attr_reader :move_count
+  
+  # Initial moves for a Buoy game. If it is an empty array, the game is
+  # normal with the initial setting; otherwise, the game is started after the
+  # moves.
+  attr_reader :initial_moves
+
+  # A move parsed by handle_one_move. If the move is not :normal, the board
+  # position may or may not be rolled back.
+  #
+  attr_reader :move
+
+  # See if self equals rhs, including a logical board position (i.e.
+  # not see object IDs) and sennichite stuff.
+  #
+  def ==(rhs)
+    return @teban         == rhs.teban &&
+           @move_count    == rhs.move_count &&
+           to_s           == rhs.to_s &&
+           @history       == rhs.history &&
+           @sente_history == rhs.sente_history &&
+           @gote_history  == rhs.gote_history &&
+           @initial_moves == rhs.initial_moves
+  end
 
   def deep_copy
-    # return Marshal.load(Marshal.dump(self))
-    board = Board.new(self.move_count)
-    board.sente_hands   = self.sente_hands.clone
-    board.gote_hands    = self.gote_hands.clone
-    board.history       = self.history.clone
-    board.history.default = 0
-    board.sente_history = self.sente_history.clone
-    board.sente_history.default = 0
-    board.gote_history  = self.gote_history.clone
-    board.gote_history.default = 0
-    board.array = []
-    self.array.each {|a| board.array.push(a.clone)}
-    board.teban         = self.teban
-    return board
+    return Marshal.load(Marshal.dump(self))
   end
 
   def initial
@@ -83,6 +160,139 @@ class Board
     @teban = true
   end
 
+  # Cache OU piece.
+  # Piece#new will call back this method.
+  #
+  def add_ou(ou)
+    if ou.sente
+      @ous[0] = ou
+    else
+      @ous[1] = ou
+    end
+  end
+
+  # Set up a board with the strs.
+  # Failing to parse the moves raises an StandardError.
+  # @param strs a board text
+  #
+  def set_from_str(strs)
+    strs.each_line do |str|
+      case str
+      when /^P\d/
+        str.sub!(/^P(.)/, '')
+        y = $1.to_i
+        x = 9
+        while (str.length > 2)
+          str.sub!(/^(...?)/, '')
+          one = $1
+          if (one =~ /^([\+\-])(..)/)
+            sg = $1
+            name = $2
+            if (sg == "+")
+              sente = true
+            else
+              sente = false
+            end
+            if ((x < 1) || (9 < x) || (y < 1) || (9 < y))
+              raise "bad position #{x} #{y}"
+            end
+            case (name)
+            when "FU"
+              PieceFU::new(self, x, y, sente)
+            when "KY"
+              PieceKY::new(self, x, y, sente)
+            when "KE"
+              PieceKE::new(self, x, y, sente)
+            when "GI"
+              PieceGI::new(self, x, y, sente)
+            when "KI"
+              PieceKI::new(self, x, y, sente)
+            when "OU"
+              PieceOU::new(self, x, y, sente)
+            when "KA"
+              PieceKA::new(self, x, y, sente)
+            when "HI"
+              PieceHI::new(self, x, y, sente)
+            when "TO"
+              PieceFU::new(self, x, y, sente, true)
+            when "NY"
+              PieceKY::new(self, x, y, sente, true)
+            when "NK"
+              PieceKE::new(self, x, y, sente, true)
+            when "NG"
+              PieceGI::new(self, x, y, sente, true)
+            when "UM"
+              PieceKA::new(self, x, y, sente, true)
+            when "RY"
+              PieceHI::new(self, x, y, sente, true)
+            else
+              raise "unkown piece #{name}"
+            end
+          end
+          x = x - 1
+        end
+      when /^P([\+\-])/
+        sg = $1
+        if (sg == "+")
+          sente = true
+        else
+          sente = false
+        end
+        str.sub!(/^../, '')
+        while (str.length > 3)
+          str.sub!(/^..(..)/, '')
+          name = $1
+          case (name)
+          when "FU"
+            PieceFU::new(self, 0, 0, sente)
+          when "KY"
+            PieceKY::new(self, 0, 0, sente)
+          when "KE"
+            PieceKE::new(self, 0, 0, sente)
+          when "GI"
+            PieceGI::new(self, 0, 0, sente)
+          when "KI"
+            PieceKI::new(self, 0, 0, sente)
+          when "KA"
+            PieceKA::new(self, 0, 0, sente)
+          when "HI"
+            PieceHI::new(self, 0, 0, sente)
+          else
+            raise "unkown piece #{name}"
+          end
+        end # while
+      when /^\+$/
+        @teban = true
+      when /^\-$/
+        @teban = false
+      else
+        raise "bad line: #{str}"
+      end # case
+    end # do
+  end
+
+  # Set up a board starting with a position after the moves.
+  # Failing to parse the moves raises an ArgumentError.
+  # @param moves an array of moves. ex. ["+7776FU", "-3334FU"] or
+  #        an array of arrays. ex. [["+7776FU","T2"], ["-3334FU","T5"]]
+  #
+  def set_from_moves(moves)
+    initial()
+    return :normal if moves.empty?
+    rt = nil
+    moves.each do |move|
+      rt = nil
+      case move
+      when Array
+        rt = handle_one_move(move[0], @teban)
+      when String
+        rt = handle_one_move(move, @teban)
+      end
+      raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
+    end
+    @initial_moves = moves.dup
+  end
+
   def have_piece?(hands, name)
     piece = hands.find { |i|
       i.name == name
@@ -90,14 +300,17 @@ class Board
     return piece
   end
 
-  def move_to(x0, y0, x1, y1, name, sente)
+  # :illegal and :outori do not change this instance (self).
+  #
+  def move_to(move)
+    x0, x1, y0, y1, name, sente = move.x0, move.x1, move.y0, move.y1, move.name, move.sente
     if (sente)
       hands = @sente_hands
     else
       hands = @gote_hands
     end
 
-    if ((x0 == 0) || (y0 == 0))
+    if move.is_drop?
       piece = have_piece?(hands, name)
       return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
       piece.move_to(x1, y1)
@@ -105,13 +318,18 @@ class Board
       if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
         return :illegal
       end
-      if (@array[x0][y0].name != name) # promoted ?
+      if (@array[x1][y1] && @array[x1][y1].name == "OU")
+          return :outori
+      end
+
+      # Change the state of this instance (self)
+      if (@array[x0][y0].name != name && !@array[x0][y0].promoted)
+        # the piece is promoting
         @array[x0][y0].promoted = true
+        move.promotion = true
       end
       if (@array[x1][y1]) # capture
-        if (@array[x1][y1].name == "OU")
-          return :outori        # return board update
-        end
+        move.set_captured_piece(@array[x1][y1])
         @array[x1][y1].sente = @array[x0][y0].sente
         @array[x1][y1].move_to(0, 0)
         hands.sort! {|a, b| # TODO refactor. Move to Piece class
@@ -125,25 +343,51 @@ class Board
     return true
   end
 
-  def look_for_ou(sente)
-    x = 1
-    while (x <= 9)
-      y = 1
-      while (y <= 9)
-        if (@array[x][y] &&
-            (@array[x][y].name == "OU") &&
-            (@array[x][y].sente == sente))
-          return @array[x][y]
-        end
-        y = y + 1
+  # Get back the previous move, which moved a name piece from [x0,y0] to 
+  # [x1, y1] with or without promotion. If the move captured
+  # a piece, it is captured_piece, which is now in hand. The captured_piece
+  # was promoted or unpromoted.
+  #
+  def move_back(move)
+    if (move.sente)
+      hands = @sente_hands
+    else
+      hands = @gote_hands
+    end
+
+    piece = @array[move.x1][move.y1]
+    if move.is_drop?
+      piece.move_to(0, 0)
+    else
+      piece.move_to(move.x0, move.y0)
+      piece.promoted = false if piece.promoted && move.promotion
+      if move.captured_piece 
+        move.captured_piece.move_to(move.x1, move.y1)
+        move.captured_piece.sente = move.sente ? false : true
+        move.captured_piece.promoted = true if move.captured_piece_promoted 
       end
-      x = x + 1
     end
-    raise "can't find ou"
+    
+    @move_count -= 1
+    @teban = @teban ? false : true
+    return true
+  end
+  
+  # def each_reserved_square
+  
+  def look_for_ou(sente)
+    if sente
+      return @ous[0]
+    else
+      return @ous[1]
+    end
   end
 
-  # note checkmate, but check. sente is checked.
-  def checkmated?(sente)        # sente is loosing
+  # See if sente is checked (i.e. loosing) or not.
+  # Note that the method name "checkmated?" is wrong. Instead, it should be
+  # "checked?" 
+  #
+  def checkmated?(sente)
     ou = look_for_ou(sente)
     x = 1
     while (x <= 9)
@@ -162,6 +406,8 @@ class Board
     return false
   end
 
+  # See if sente's FU drop checkmates the opponent or not.
+  #
   def uchifuzume?(sente)
     rival_ou = look_for_ou(! sente)   # rival's ou
     if (sente)                  # rival is gote
@@ -189,7 +435,7 @@ class Board
     ## case: rival_ou is moving
     rival_ou.movable_grids.each do |(cand_x, cand_y)|
       tmp_board = deep_copy
-      s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
+      s = tmp_board.move_to(Move.new(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente))
       raise "internal error" if (s != true)
       if (! tmp_board.checkmated?(! sente)) # good move
         return false
@@ -217,7 +463,7 @@ class Board
           end
           names.map! do |name|
             tmp_board = deep_copy
-            s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
+            s = tmp_board.move_to(Move.new(x, y, fu_x, fu_y, name, ! sente))
             if s == :illegal
               s # result
             else
@@ -262,17 +508,43 @@ class Board
     end
   end
 
+  # Deep-copy sennichite stuff, which will later be available to restore.
+  #
+  def dup_sennichite_stuff
+    return [@history.dup, @sente_history.dup, @gote_history.dup]
+  end
+
+  # Restore sennihite stuff.
+  #
+  def restore_sennichite_stuff(history, sente_history, gote_history)
+    @history, @sente_history, @gote_history = history, sente_history, gote_history
+  end
+
   def oute_sennichite?(player)
-    if (@sente_history[to_s] >= 4)
-      return :oute_sennichite_sente_lose
-    elsif (@gote_history[to_s] >= 4)
-      return :oute_sennichite_gote_lose
+    return nil unless sennichite?
+
+    if player
+      # sente's turn
+      if (@sente_history[to_s] >= 4)   # sente is checking gote
+        return :oute_sennichite_sente_lose
+      elsif (@gote_history[to_s] >= 3) # sente is escaping
+        return :oute_sennichite_gote_lose
+      else
+        return nil # Not oute_sennichite, but sennichite
+      end
     else
-      return nil
+      # gote's turn
+      if (@gote_history[to_s] >= 4)     # gote is checking sente
+        return :oute_sennichite_gote_lose
+      elsif (@sente_history[to_s] >= 3) # gote is escaping
+        return :oute_sennichite_sente_lose
+      else
+        return nil # Not oute_sennichite, but sennichite
+      end
     end
   end
 
-  def sennichite?(sente)
+  def sennichite?
     if (@history[to_s] >= 4) # already 3 times
       return true
     end
@@ -342,6 +614,20 @@ class Board
   end
 
   # sente is nil only if tests in test_board run
+  # @return
+  #   - :normal
+  #   - :toryo 
+  #   - :kachi_win 
+  #   - :kachi_lose 
+  #   - :sennichite 
+  #   - :oute_sennichite_sente_lose 
+  #   - :oute_sennichite_gote_lose 
+  #   - :illegal 
+  #   - :uchifuzume 
+  #   - :oute_kaihimore 
+  #   - (:outori will not be returned)
+  #   - :max_moves
+  #
   def handle_one_move(str, sente=nil)
     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
       sg = $1
@@ -399,24 +685,48 @@ class Board
       return :illegal           # can't put on existing piece
     end
 
-    tmp_board = deep_copy
-    return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
-    return :oute_kaihimore if (tmp_board.checkmated?(sente))
-    tmp_board.update_sennichite(sente)
-    os_result = tmp_board.oute_sennichite?(sente)
-    return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
-    return :sennichite if tmp_board.sennichite?(sente)
-
-    if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
+    @move = Move.new(x0, y0, x1, y1, name, sente)
+    result = move_to(@move)
+    if (result == :illegal)
+      # self is unchanged
+      return :illegal 
+    end
+    if (checkmated?(sente))
+      move_back(@move)
+      return :oute_kaihimore 
+    end
+    if ((x0 == 0) && (y0 == 0) && (name == "FU") && uchifuzume?(sente))
+      move_back(@move)
       return :uchifuzume
     end
 
-    move_to(x0, y0, x1, y1, name, sente)
-
+    sennichite_stuff = dup_sennichite_stuff
     update_sennichite(sente)
+    os_result = oute_sennichite?(sente)
+    if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
+      move_back(@move)
+      restore_sennichite_stuff(*sennichite_stuff)
+      return os_result 
+    end
+    if sennichite?
+      move_back(@move)
+      restore_sennichite_stuff(*sennichite_stuff)
+      return :sennichite 
+    end
+
+    # New rule that CSA introduced in November 2014.
+    # If a game with 256 plies does not end, make the game a draw.
+    # When running test cases $options might be nil.
+    if $options && $options["max-moves"] &&
+       $options["max-moves"] > 0 && @move_count >= $options["max-moves"]
+      return :max_moves
+    end
+
     return :normal
   end
 
+  # Return a CSA-styled string notation of the current position.
+  #
   def to_s
     a = Array::new
     y = 1
@@ -453,6 +763,14 @@ class Board
     a.push("%s\n" % [@teban ? "+" : "-"])
     return a.join
   end
+
+  # Return a CSA-styled string notation of the initial position.
+  #
+  def initial_string
+    tmp_board = self.class.new
+    tmp_board.initial
+    return tmp_board.to_s
+  end
 end
 
 end # ShogiServer