## $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/piece'
+require 'shogi_server/move'
module ShogiServer # for a namespace
+class WrongMoves < ArgumentError; end
+
class Board
- def initialize
+
+ # 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
@history = Hash::new(0)
@sente_history = Hash::new(0)
@gote_history = Hash::new(0)
@array = [[], [], [], [], [], [], [], [], [], []]
- @move_count = 0
+ @move_count = move_count
@teban = nil # black => true, white => false
+ @initial_moves = []
+ @ous = [nil, nil] # keep OU pieces of Sente and Gote
end
- attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
+ 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
+
+ # 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))
+ end
def initial
PieceKY::new(self, 1, 1, false)
@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
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.move_to?(x1, y1, name)) # TODO null check for the piece?
+ return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
piece.move_to(x1, y1)
else
- return :illegal if (! @array[x0][y0].move_to?(x1, y1, name)) # TODO null check?
- if (@array[x0][y0].name != name) # promoted ?
+ if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
+ return :illegal
+ end
+ 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
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)
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
## case: rival_ou is moving
rival_ou.movable_grids.each do |(cand_x, cand_y)|
- tmp_board = Marshal.load(Marshal.dump(self))
- s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
+ tmp_board = deep_copy
+ 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
end
end
names.map! do |name|
- tmp_board = Marshal.load(Marshal.dump(self))
- s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
+ tmp_board = deep_copy
+ s = tmp_board.move_to(Move.new(x, y, fu_x, fu_y, name, ! sente))
if s == :illegal
s # result
else
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
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)
+ #
def handle_one_move(str, sente=nil)
if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
sg = $1
return :illegal # can't put on existing piece
end
- tmp_board = Marshal.load(Marshal.dump(self))
- 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
+
return :normal
end
+ # Return a CSA-styled string notation of the current position.
+ #
def to_s
a = Array::new
y = 1
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