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 module ShogiServer # for a namespace
22 class WrongMoves < ArgumentError; end
26 # Split a moves line into an array of a move string.
27 # If it fails to parse the moves, it raises WrongMoves.
28 # @param moves a moves line. Ex. "+776FU-3334Fu"
29 # @return an array of a move string. Ex. ["+7776FU", "-3334FU"]
31 def Board.split_moves(moves)
34 rs = moves.gsub %r{[\+\-]\d{4}\w{2}} do |s|
38 raise WrongMoves, rs unless rs.empty?
43 def initialize(move_count=0)
44 @sente_hands = Array::new
45 @gote_hands = Array::new
46 @history = Hash::new(0)
47 @sente_history = Hash::new(0)
48 @gote_history = Hash::new(0)
49 @array = [[], [], [], [], [], [], [], [], [], []]
50 @move_count = move_count
51 @teban = nil # black => true, white => false
54 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
55 attr_reader :move_count
57 # Initial moves for a Buoy game. If it is an empty array, the game is
58 # normal with the initial setting; otherwise, the game is started after the
60 attr_reader :initial_moves
63 return Marshal.load(Marshal.dump(self))
67 PieceKY::new(self, 1, 1, false)
68 PieceKE::new(self, 2, 1, false)
69 PieceGI::new(self, 3, 1, false)
70 PieceKI::new(self, 4, 1, false)
71 PieceOU::new(self, 5, 1, false)
72 PieceKI::new(self, 6, 1, false)
73 PieceGI::new(self, 7, 1, false)
74 PieceKE::new(self, 8, 1, false)
75 PieceKY::new(self, 9, 1, false)
76 PieceKA::new(self, 2, 2, false)
77 PieceHI::new(self, 8, 2, false)
79 PieceFU::new(self, i, 3, false)
82 PieceKY::new(self, 1, 9, true)
83 PieceKE::new(self, 2, 9, true)
84 PieceGI::new(self, 3, 9, true)
85 PieceKI::new(self, 4, 9, true)
86 PieceOU::new(self, 5, 9, true)
87 PieceKI::new(self, 6, 9, true)
88 PieceGI::new(self, 7, 9, true)
89 PieceKE::new(self, 8, 9, true)
90 PieceKY::new(self, 9, 9, true)
91 PieceKA::new(self, 8, 8, true)
92 PieceHI::new(self, 2, 8, true)
94 PieceFU::new(self, i, 7, true)
99 # Set up a board with the strs.
100 # Failing to parse the moves raises an StandardError.
101 # @param strs a board text
103 def set_from_str(strs)
104 strs.each_line do |str|
107 str.sub!(/^P(.)/, '')
110 while (str.length > 2)
111 str.sub!(/^(...?)/, '')
113 if (one =~ /^([\+\-])(..)/)
121 if ((x < 1) || (9 < x) || (y < 1) || (9 < y))
122 raise "bad position #{x} #{y}"
126 PieceFU::new(self, x, y, sente)
128 PieceKY::new(self, x, y, sente)
130 PieceKE::new(self, x, y, sente)
132 PieceGI::new(self, x, y, sente)
134 PieceKI::new(self, x, y, sente)
136 PieceOU::new(self, x, y, sente)
138 PieceKA::new(self, x, y, sente)
140 PieceHI::new(self, x, y, sente)
142 PieceFU::new(self, x, y, sente, true)
144 PieceKY::new(self, x, y, sente, true)
146 PieceKE::new(self, x, y, sente, true)
148 PieceGI::new(self, x, y, sente, true)
150 PieceKA::new(self, x, y, sente, true)
152 PieceHI::new(self, x, y, sente, true)
154 raise "unkown piece #{name}"
167 while (str.length > 3)
168 str.sub!(/^..(..)/, '')
172 PieceFU::new(self, 0, 0, sente)
174 PieceKY::new(self, 0, 0, sente)
176 PieceKE::new(self, 0, 0, sente)
178 PieceGI::new(self, 0, 0, sente)
180 PieceKI::new(self, 0, 0, sente)
182 PieceKA::new(self, 0, 0, sente)
184 PieceHI::new(self, 0, 0, sente)
186 raise "unkown piece #{name}"
194 raise "bad line: #{str}"
199 # Set up a board starting with a position after the moves.
200 # Failing to parse the moves raises an ArgumentError.
201 # @param moves an array of moves. ex. ["+7776FU", "-3334FU"]
203 def set_from_moves(moves)
205 return :normal if moves.empty?
208 rt = handle_one_move(move, @teban)
209 raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
211 @initial_moves = moves.dup
214 def have_piece?(hands, name)
215 piece = hands.find { |i|
221 def move_to(x0, y0, x1, y1, name, sente)
228 if ((x0 == 0) || (y0 == 0))
229 piece = have_piece?(hands, name)
230 return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
231 piece.move_to(x1, y1)
233 if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
236 if (@array[x0][y0].name != name) # promoted ?
237 @array[x0][y0].promoted = true
239 if (@array[x1][y1]) # capture
240 if (@array[x1][y1].name == "OU")
241 return :outori # return board update
243 @array[x1][y1].sente = @array[x0][y0].sente
244 @array[x1][y1].move_to(0, 0)
245 hands.sort! {|a, b| # TODO refactor. Move to Piece class
249 @array[x0][y0].move_to(x1, y1)
252 @teban = @teban ? false : true
256 def look_for_ou(sente)
262 (@array[x][y].name == "OU") &&
263 (@array[x][y].sente == sente))
270 raise "can't find ou"
273 # not checkmate, but check. sente is checked.
274 def checkmated?(sente) # sente is loosing
275 ou = look_for_ou(sente)
281 (@array[x][y].sente != sente))
282 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
293 def uchifuzume?(sente)
294 rival_ou = look_for_ou(! sente) # rival's ou
295 if (sente) # rival is gote
296 if ((rival_ou.y != 9) &&
297 (@array[rival_ou.x][rival_ou.y + 1]) &&
298 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
299 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
301 fu_y = rival_ou.y + 1
306 if ((rival_ou.y != 1) &&
307 (@array[rival_ou.x][rival_ou.y - 1]) &&
308 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
309 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
311 fu_y = rival_ou.y - 1
317 ## case: rival_ou is moving
318 rival_ou.movable_grids.each do |(cand_x, cand_y)|
319 tmp_board = deep_copy
320 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
321 raise "internal error" if (s != true)
322 if (! tmp_board.checkmated?(! sente)) # good move
327 ## case: rival is capturing fu
333 (@array[x][y].sente != sente) &&
334 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
337 if (@array[x][y].promoted)
338 names << @array[x][y].promoted_name
340 names << @array[x][y].name
341 if @array[x][y].promoted_name &&
342 @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
343 names << @array[x][y].promoted_name
347 tmp_board = deep_copy
348 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
352 tmp_board.checkmated?(! sente) # result
355 all_illegal = names.find {|a| a != :illegal}
356 raise "internal error: legal move not found" if all_illegal == nil
357 r = names.find {|a| a == false} # good move
358 return false if r == false # found good move
367 # @[sente|gote]_history has at least one item while the player is checking the other or
369 def update_sennichite(player)
372 if checkmated?(!player)
374 @sente_history["dummy"] = 1 # flag to see Sente player is checking Gote player
376 @gote_history["dummy"] = 1 # flag to see Gote player is checking Sente player
380 @sente_history.clear # no more continuous check
382 @gote_history.clear # no more continuous check
385 if @sente_history.size > 0 # possible for Sente's or Gote's turn
386 @sente_history[str] += 1
388 if @gote_history.size > 0 # possible for Sente's or Gote's turn
389 @gote_history[str] += 1
393 def oute_sennichite?(player)
394 return nil unless sennichite?
398 if (@sente_history[to_s] >= 4) # sente is checking gote
399 return :oute_sennichite_sente_lose
400 elsif (@gote_history[to_s] >= 3) # sente is escaping
401 return :oute_sennichite_gote_lose
403 return nil # Not oute_sennichite, but sennichite
407 if (@gote_history[to_s] >= 4) # gote is checking sente
408 return :oute_sennichite_gote_lose
409 elsif (@sente_history[to_s] >= 3) # gote is escaping
410 return :oute_sennichite_sente_lose
412 return nil # Not oute_sennichite, but sennichite
418 if (@history[to_s] >= 4) # already 3 times
424 def good_kachi?(sente)
425 if (checkmated?(sente))
426 puts "'NG: Checkmating." if $DEBUG
430 ou = look_for_ou(sente)
431 if (sente && (ou.y >= 4))
432 puts "'NG: Black's OU does not enter yet." if $DEBUG
435 if (! sente && (ou.y <= 6))
436 puts "'NG: White's OU does not enter yet." if $DEBUG
454 (@array[x][y].sente == sente) &&
455 (@array[x][y].point > 0))
456 point = point + @array[x][y].point
462 hands.each do |piece|
463 point = point + piece.point
467 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
472 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
477 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
482 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
486 # sente is nil only if tests in test_board run
487 def handle_one_move(str, sente=nil)
488 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
495 elsif (str =~ /^%KACHI/)
496 raise ArgumentError, "sente is null", caller if sente == nil
497 if (good_kachi?(sente))
502 elsif (str =~ /^%TORYO/)
508 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
509 ((x0 != 0) || (y0 != 0)))
511 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
516 sente = true if sente == nil # deprecated
517 return :illegal unless sente == true # black player's move must be black
520 sente = false if sente == nil # deprecated
521 return :illegal unless sente == false # white player's move must be white
526 if ((x0 == 0) && (y0 == 0))
527 return :illegal if (! have_piece?(hands, name))
528 elsif (! @array[x0][y0])
529 return :illegal # no piece
530 elsif (@array[x0][y0].sente != sente)
531 return :illegal # this is not mine
532 elsif (@array[x0][y0].name != name)
533 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
537 if (@array[x1][y1] &&
538 (@array[x1][y1].sente == sente)) # can't capture mine
540 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
541 return :illegal # can't put on existing piece
544 tmp_board = deep_copy
545 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
546 return :oute_kaihimore if (tmp_board.checkmated?(sente))
547 tmp_board.update_sennichite(sente)
548 os_result = tmp_board.oute_sennichite?(sente)
549 return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
550 return :sennichite if tmp_board.sennichite?
552 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
556 move_to(x0, y0, x1, y1, name, sente)
558 update_sennichite(sente)
566 a.push(sprintf("P%d", y))
578 a.push(sprintf("\n"))
581 if (! sente_hands.empty?)
583 sente_hands.each do |p|
584 a.push("00" + p.name)
588 if (! gote_hands.empty?)
590 gote_hands.each do |p|
591 a.push("00" + p.name)
595 a.push("%s\n" % [@teban ? "+" : "-"])