X-Git-Url: http://git.sourceforge.jp/view?p=shogi-server%2Fshogi-server.git;a=blobdiff_plain;f=shogi_server%2Fboard.rb;h=3bd01b1b6cee33553c349f235182dda989dea182;hp=379dc6929276947e277a9d0e4656b93de734c9b9;hb=32adcedbca9b794239ae0d2c63fb248dc8102372;hpb=d1a385928c33bc160b9a2d77994c8f5f4c876fe7 diff --git a/shogi_server/board.rb b/shogi_server/board.rb index 379dc69..3bd01b1 100644 --- a/shogi_server/board.rb +++ b/shogi_server/board.rb @@ -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 @@ -17,9 +17,73 @@ ## 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 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, :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)) @@ -70,6 +154,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 @@ -77,14 +294,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) @@ -92,13 +312,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 @@ -112,25 +337,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) @@ -149,6 +400,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 @@ -176,7 +429,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 @@ -204,7 +457,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 @@ -249,17 +502,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 @@ -329,6 +608,19 @@ 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) + # def handle_one_move(str, sente=nil) if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/) sg = $1 @@ -386,24 +678,40 @@ 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 + return :normal end + # Return a CSA-styled string notation of the current position. + # def to_s a = Array::new y = 1 @@ -440,6 +748,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