OSDN Git Service

Enhanced syntax of Floodgate time configuration file. Now it supports "set sacrifice...
[shogi-server/shogi-server.git] / shogi_server / board.rb
1 ## $Id$
2
3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
5 ##
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.
10 ##
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.
15 ##
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
19
20 require 'shogi_server/move'
21
22 module ShogiServer # for a namespace
23
24 class WrongMoves < ArgumentError; end
25
26 class Board
27   
28   # Initial board setup. 
29   # The string ends with '+', not a line break.
30   #
31   INITIAL_HIRATE_POSITION = (<<-EOF).chomp
32 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
33 P2 * -HI *  *  *  *  * -KA * 
34 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
35 P4 *  *  *  *  *  *  *  *  * 
36 P5 *  *  *  *  *  *  *  *  * 
37 P6 *  *  *  *  *  *  *  *  * 
38 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
39 P8 * +KA *  *  *  *  * +HI * 
40 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
41 +
42 EOF
43
44   # Split a moves line into an array of a move string.
45   # If it fails to parse the moves, it raises WrongMoves.
46   # @param moves a moves line. Ex. "+776FU-3334FU" or
47   #              moves with times. Ex "+776FU,T2-3334FU,T5"
48   # @return an array of a move string. Ex. ["+7776FU", "-3334FU"] or
49   #         an array of arrays. Ex. [["+7776FU","T2"], ["-3334FU", "T5"]]
50   #
51   def Board.split_moves(moves)
52     ret = []
53
54     i=0
55     tmp = ""
56     while i<moves.size
57       if moves[i,1] == "+" ||
58          moves[i,1] == "-" ||
59          i == moves.size - 1
60         if i == moves.size - 1
61           tmp << moves[i,1]
62         end
63         unless tmp.empty?
64           a = tmp.split(",")
65           if a[0].size != 7
66             raise WrongMoves, a[0]
67           end
68           if a.size == 1 # "+7776FU"
69             ret << a[0]
70           else           # "+7776FU,T2"
71             unless /^T\d+/ =~ a[1] 
72               raise WrongMoves, a[1]
73             end
74             ret << a
75           end
76           tmp = ""
77         end
78       end
79       tmp << moves[i,1]
80       i += 1
81     end
82
83     return ret
84   end
85
86
87   def initialize(move_count=0)
88     @sente_hands = Array::new
89     @gote_hands  = Array::new
90     @history       = Hash::new(0)
91     @sente_history = Hash::new(0)
92     @gote_history  = Hash::new(0)
93     @array = [[], [], [], [], [], [], [], [], [], []]
94     @move_count = move_count
95     @teban = nil # black => true, white => false
96     @initial_moves = []
97     @ous = [nil, nil] # keep OU pieces of Sente and Gote
98   end
99   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
100   attr_reader :move_count
101   
102   # Initial moves for a Buoy game. If it is an empty array, the game is
103   # normal with the initial setting; otherwise, the game is started after the
104   # moves.
105   attr_reader :initial_moves
106
107   # See if self equals rhs, including a logical board position (i.e.
108   # not see object IDs) and sennichite stuff.
109   #
110   def ==(rhs)
111     return @teban         == rhs.teban &&
112            @move_count    == rhs.move_count &&
113            to_s           == rhs.to_s &&
114            @history       == rhs.history &&
115            @sente_history == rhs.sente_history &&
116            @gote_history  == rhs.gote_history &&
117            @initial_moves == rhs.initial_moves
118   end
119
120   def deep_copy
121     return Marshal.load(Marshal.dump(self))
122   end
123
124   def initial
125     PieceKY::new(self, 1, 1, false)
126     PieceKE::new(self, 2, 1, false)
127     PieceGI::new(self, 3, 1, false)
128     PieceKI::new(self, 4, 1, false)
129     PieceOU::new(self, 5, 1, false)
130     PieceKI::new(self, 6, 1, false)
131     PieceGI::new(self, 7, 1, false)
132     PieceKE::new(self, 8, 1, false)
133     PieceKY::new(self, 9, 1, false)
134     PieceKA::new(self, 2, 2, false)
135     PieceHI::new(self, 8, 2, false)
136     (1..9).each do |i|
137       PieceFU::new(self, i, 3, false)
138     end
139
140     PieceKY::new(self, 1, 9, true)
141     PieceKE::new(self, 2, 9, true)
142     PieceGI::new(self, 3, 9, true)
143     PieceKI::new(self, 4, 9, true)
144     PieceOU::new(self, 5, 9, true)
145     PieceKI::new(self, 6, 9, true)
146     PieceGI::new(self, 7, 9, true)
147     PieceKE::new(self, 8, 9, true)
148     PieceKY::new(self, 9, 9, true)
149     PieceKA::new(self, 8, 8, true)
150     PieceHI::new(self, 2, 8, true)
151     (1..9).each do |i|
152       PieceFU::new(self, i, 7, true)
153     end
154     @teban = true
155   end
156
157   # Cache OU piece.
158   # Piece#new will call back this method.
159   #
160   def add_ou(ou)
161     if ou.sente
162       @ous[0] = ou
163     else
164       @ous[1] = ou
165     end
166   end
167
168   # Set up a board with the strs.
169   # Failing to parse the moves raises an StandardError.
170   # @param strs a board text
171   #
172   def set_from_str(strs)
173     strs.each_line do |str|
174       case str
175       when /^P\d/
176         str.sub!(/^P(.)/, '')
177         y = $1.to_i
178         x = 9
179         while (str.length > 2)
180           str.sub!(/^(...?)/, '')
181           one = $1
182           if (one =~ /^([\+\-])(..)/)
183             sg = $1
184             name = $2
185             if (sg == "+")
186               sente = true
187             else
188               sente = false
189             end
190             if ((x < 1) || (9 < x) || (y < 1) || (9 < y))
191               raise "bad position #{x} #{y}"
192             end
193             case (name)
194             when "FU"
195               PieceFU::new(self, x, y, sente)
196             when "KY"
197               PieceKY::new(self, x, y, sente)
198             when "KE"
199               PieceKE::new(self, x, y, sente)
200             when "GI"
201               PieceGI::new(self, x, y, sente)
202             when "KI"
203               PieceKI::new(self, x, y, sente)
204             when "OU"
205               PieceOU::new(self, x, y, sente)
206             when "KA"
207               PieceKA::new(self, x, y, sente)
208             when "HI"
209               PieceHI::new(self, x, y, sente)
210             when "TO"
211               PieceFU::new(self, x, y, sente, true)
212             when "NY"
213               PieceKY::new(self, x, y, sente, true)
214             when "NK"
215               PieceKE::new(self, x, y, sente, true)
216             when "NG"
217               PieceGI::new(self, x, y, sente, true)
218             when "UM"
219               PieceKA::new(self, x, y, sente, true)
220             when "RY"
221               PieceHI::new(self, x, y, sente, true)
222             else
223               raise "unkown piece #{name}"
224             end
225           end
226           x = x - 1
227         end
228       when /^P([\+\-])/
229         sg = $1
230         if (sg == "+")
231           sente = true
232         else
233           sente = false
234         end
235         str.sub!(/^../, '')
236         while (str.length > 3)
237           str.sub!(/^..(..)/, '')
238           name = $1
239           case (name)
240           when "FU"
241             PieceFU::new(self, 0, 0, sente)
242           when "KY"
243             PieceKY::new(self, 0, 0, sente)
244           when "KE"
245             PieceKE::new(self, 0, 0, sente)
246           when "GI"
247             PieceGI::new(self, 0, 0, sente)
248           when "KI"
249             PieceKI::new(self, 0, 0, sente)
250           when "KA"
251             PieceKA::new(self, 0, 0, sente)
252           when "HI"
253             PieceHI::new(self, 0, 0, sente)
254           else
255             raise "unkown piece #{name}"
256           end
257         end # while
258       when /^\+$/
259         @teban = true
260       when /^\-$/
261         @teban = false
262       else
263         raise "bad line: #{str}"
264       end # case
265     end # do
266   end
267
268   # Set up a board starting with a position after the moves.
269   # Failing to parse the moves raises an ArgumentError.
270   # @param moves an array of moves. ex. ["+7776FU", "-3334FU"] or
271   #        an array of arrays. ex. [["+7776FU","T2"], ["-3334FU","T5"]]
272   #
273   def set_from_moves(moves)
274     initial()
275     return :normal if moves.empty?
276     rt = nil
277     moves.each do |move|
278       rt = nil
279       case move
280       when Array
281         rt = handle_one_move(move[0], @teban)
282       when String
283         rt = handle_one_move(move, @teban)
284       end
285       raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
286     end
287     @initial_moves = moves.dup
288   end
289
290   def have_piece?(hands, name)
291     piece = hands.find { |i|
292       i.name == name
293     }
294     return piece
295   end
296
297   # :illegal and :outori do not change this instance (self).
298   #
299   def move_to(move)
300     x0, x1, y0, y1, name, sente = move.x0, move.x1, move.y0, move.y1, move.name, move.sente
301     if (sente)
302       hands = @sente_hands
303     else
304       hands = @gote_hands
305     end
306
307     if move.is_drop?
308       piece = have_piece?(hands, name)
309       return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
310       piece.move_to(x1, y1)
311     else
312       if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
313         return :illegal
314       end
315       if (@array[x1][y1] && @array[x1][y1].name == "OU")
316           return :outori
317       end
318
319       # Change the state of this instance (self)
320       if (@array[x0][y0].name != name && !@array[x0][y0].promoted)
321         # the piece is promoting
322         @array[x0][y0].promoted = true
323         move.promotion = true
324       end
325       if (@array[x1][y1]) # capture
326         move.set_captured_piece(@array[x1][y1])
327         @array[x1][y1].sente = @array[x0][y0].sente
328         @array[x1][y1].move_to(0, 0)
329         hands.sort! {|a, b| # TODO refactor. Move to Piece class
330           a.name <=> b.name
331         }
332       end
333       @array[x0][y0].move_to(x1, y1)
334     end
335     @move_count += 1
336     @teban = @teban ? false : true
337     return true
338   end
339
340   # Get back the previous move, which moved a name piece from [x0,y0] to 
341   # [x1, y1] with or without promotion. If the move captured
342   # a piece, it is captured_piece, which is now in hand. The captured_piece
343   # was promoted or unpromoted.
344   #
345   def move_back(move)
346     if (move.sente)
347       hands = @sente_hands
348     else
349       hands = @gote_hands
350     end
351
352     piece = @array[move.x1][move.y1]
353     if move.is_drop?
354       piece.move_to(0, 0)
355     else
356       piece.move_to(move.x0, move.y0)
357       piece.promoted = false if piece.promoted && move.promotion
358       if move.captured_piece 
359         move.captured_piece.move_to(move.x1, move.y1)
360         move.captured_piece.sente = move.sente ? false : true
361         move.captured_piece.promoted = true if move.captured_piece_promoted 
362       end
363     end
364     
365     @move_count -= 1
366     @teban = @teban ? false : true
367     return true
368   end
369   
370   # def each_reserved_square
371   
372   def look_for_ou(sente)
373     if sente
374       return @ous[0]
375     else
376       return @ous[1]
377     end
378   end
379
380   # See if sente is checked (i.e. loosing) or not.
381   # Note that the method name "checkmated?" is wrong. Instead, it should be
382   # "checked?" 
383   #
384   def checkmated?(sente)
385     ou = look_for_ou(sente)
386     x = 1
387     while (x <= 9)
388       y = 1
389       while (y <= 9)
390         if (@array[x][y] &&
391             (@array[x][y].sente != sente))
392           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
393             return true
394           end
395         end
396         y = y + 1
397       end
398       x = x + 1
399     end
400     return false
401   end
402
403   # See if sente's FU drop checkmates the opponent or not.
404   #
405   def uchifuzume?(sente)
406     rival_ou = look_for_ou(! sente)   # rival's ou
407     if (sente)                  # rival is gote
408       if ((rival_ou.y != 9) &&
409           (@array[rival_ou.x][rival_ou.y + 1]) &&
410           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
411           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
412         fu_x = rival_ou.x
413         fu_y = rival_ou.y + 1
414       else
415         return false
416       end
417     else                        # gote
418       if ((rival_ou.y != 1) &&
419           (@array[rival_ou.x][rival_ou.y - 1]) &&
420           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
421           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
422         fu_x = rival_ou.x
423         fu_y = rival_ou.y - 1
424       else
425         return false
426       end
427     end
428
429     ## case: rival_ou is moving
430     rival_ou.movable_grids.each do |(cand_x, cand_y)|
431       tmp_board = deep_copy
432       s = tmp_board.move_to(Move.new(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente))
433       raise "internal error" if (s != true)
434       if (! tmp_board.checkmated?(! sente)) # good move
435         return false
436       end
437     end
438
439     ## case: rival is capturing fu
440     x = 1
441     while (x <= 9)
442       y = 1
443       while (y <= 9)
444         if (@array[x][y] &&
445             (@array[x][y].sente != sente) &&
446             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
447           
448           names = []
449           if (@array[x][y].promoted)
450             names << @array[x][y].promoted_name
451           else
452             names << @array[x][y].name
453             if @array[x][y].promoted_name && 
454                @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
455               names << @array[x][y].promoted_name 
456             end
457           end
458           names.map! do |name|
459             tmp_board = deep_copy
460             s = tmp_board.move_to(Move.new(x, y, fu_x, fu_y, name, ! sente))
461             if s == :illegal
462               s # result
463             else
464               tmp_board.checkmated?(! sente) # result
465             end
466           end
467           all_illegal = names.find {|a| a != :illegal}
468           raise "internal error: legal move not found" if all_illegal == nil
469           r = names.find {|a| a == false} # good move
470           return false if r == false # found good move
471         end
472         y = y + 1
473       end
474       x = x + 1
475     end
476     return true
477   end
478
479   # @[sente|gote]_history has at least one item while the player is checking the other or 
480   # the other escapes.
481   def update_sennichite(player)
482     str = to_s
483     @history[str] += 1
484     if checkmated?(!player)
485       if (player)
486         @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
487       else
488         @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
489       end
490     else
491       if (player)
492         @sente_history.clear # no more continuous check
493       else
494         @gote_history.clear  # no more continuous check
495       end
496     end
497     if @sente_history.size > 0  # possible for Sente's or Gote's turn
498       @sente_history[str] += 1
499     end
500     if @gote_history.size > 0   # possible for Sente's or Gote's turn
501       @gote_history[str] += 1
502     end
503   end
504
505   # Deep-copy sennichite stuff, which will later be available to restore.
506   #
507   def dup_sennichite_stuff
508     return [@history.dup, @sente_history.dup, @gote_history.dup]
509   end
510
511   # Restore sennihite stuff.
512   #
513   def restore_sennichite_stuff(history, sente_history, gote_history)
514     @history, @sente_history, @gote_history = history, sente_history, gote_history
515   end
516
517   def oute_sennichite?(player)
518     return nil unless sennichite?
519
520     if player
521       # sente's turn
522       if (@sente_history[to_s] >= 4)   # sente is checking gote
523         return :oute_sennichite_sente_lose
524       elsif (@gote_history[to_s] >= 3) # sente is escaping
525         return :oute_sennichite_gote_lose
526       else
527         return nil # Not oute_sennichite, but sennichite
528       end
529     else
530       # gote's turn
531       if (@gote_history[to_s] >= 4)     # gote is checking sente
532         return :oute_sennichite_gote_lose
533       elsif (@sente_history[to_s] >= 3) # gote is escaping
534         return :oute_sennichite_sente_lose
535       else
536         return nil # Not oute_sennichite, but sennichite
537       end
538     end
539   end
540
541   def sennichite?
542     if (@history[to_s] >= 4) # already 3 times
543       return true
544     end
545     return false
546   end
547
548   def good_kachi?(sente)
549     if (checkmated?(sente))
550       puts "'NG: Checkmating." if $DEBUG
551       return false 
552     end
553     
554     ou = look_for_ou(sente)
555     if (sente && (ou.y >= 4))
556       puts "'NG: Black's OU does not enter yet." if $DEBUG
557       return false     
558     end  
559     if (! sente && (ou.y <= 6))
560       puts "'NG: White's OU does not enter yet." if $DEBUG
561       return false 
562     end
563       
564     number = 0
565     point = 0
566
567     if (sente)
568       hands = @sente_hands
569       r = [1, 2, 3]
570     else
571       hands = @gote_hands
572       r = [7, 8, 9]
573     end
574     r.each do |y|
575       x = 1
576       while (x <= 9)
577         if (@array[x][y] &&
578             (@array[x][y].sente == sente) &&
579             (@array[x][y].point > 0))
580           point = point + @array[x][y].point
581           number = number + 1
582         end
583         x = x + 1
584       end
585     end
586     hands.each do |piece|
587       point = point + piece.point
588     end
589
590     if (number < 10)
591       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
592       return false     
593     end  
594     if (sente)
595       if (point < 28)
596         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
597         return false 
598       end  
599     else
600       if (point < 27)
601         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
602         return false 
603       end
604     end
605
606     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
607     return true
608   end
609
610   # sente is nil only if tests in test_board run
611   # @return
612   #   - :normal
613   #   - :toryo 
614   #   - :kachi_win 
615   #   - :kachi_lose 
616   #   - :sennichite 
617   #   - :oute_sennichite_sente_lose 
618   #   - :oute_sennichite_gote_lose 
619   #   - :illegal 
620   #   - :uchifuzume 
621   #   - :oute_kaihimore 
622   #   - (:outori will not be returned)
623   #
624   def handle_one_move(str, sente=nil)
625     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
626       sg = $1
627       x0 = $2.to_i
628       y0 = $3.to_i
629       x1 = $4.to_i
630       y1 = $5.to_i
631       name = $6
632     elsif (str =~ /^%KACHI/)
633       raise ArgumentError, "sente is null", caller if sente == nil
634       if (good_kachi?(sente))
635         return :kachi_win
636       else
637         return :kachi_lose
638       end
639     elsif (str =~ /^%TORYO/)
640       return :toryo
641     else
642       return :illegal
643     end
644     
645     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
646         ((x0 != 0) || (y0 != 0)))
647       return :illegal
648     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
649       return :illegal
650     end
651     
652     if (sg == "+")
653       sente = true if sente == nil           # deprecated
654       return :illegal unless sente == true   # black player's move must be black
655       hands = @sente_hands
656     else
657       sente = false if sente == nil          # deprecated
658       return :illegal unless sente == false  # white player's move must be white
659       hands = @gote_hands
660     end
661     
662     ## source check
663     if ((x0 == 0) && (y0 == 0))
664       return :illegal if (! have_piece?(hands, name))
665     elsif (! @array[x0][y0])
666       return :illegal           # no piece
667     elsif (@array[x0][y0].sente != sente)
668       return :illegal           # this is not mine
669     elsif (@array[x0][y0].name != name)
670       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
671     end
672
673     ## destination check
674     if (@array[x1][y1] &&
675         (@array[x1][y1].sente == sente)) # can't capture mine
676       return :illegal
677     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
678       return :illegal           # can't put on existing piece
679     end
680
681     move = Move.new(x0, y0, x1, y1, name, sente)
682     result = move_to(move)
683     if (result == :illegal)
684       # self is unchanged
685       return :illegal 
686     end
687     if (checkmated?(sente))
688       move_back(move)
689       return :oute_kaihimore 
690     end
691     if ((x0 == 0) && (y0 == 0) && (name == "FU") && uchifuzume?(sente))
692       move_back(move)
693       return :uchifuzume
694     end
695
696     sennichite_stuff = dup_sennichite_stuff
697     update_sennichite(sente)
698     os_result = oute_sennichite?(sente)
699     if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
700       move_back(move)
701       restore_sennichite_stuff(*sennichite_stuff)
702       return os_result 
703     end
704     if sennichite?
705       move_back(move)
706       restore_sennichite_stuff(*sennichite_stuff)
707       return :sennichite 
708     end
709
710     return :normal
711   end
712
713   # Return a CSA-styled string notation of the current position.
714   #
715   def to_s
716     a = Array::new
717     y = 1
718     while (y <= 9)
719       a.push(sprintf("P%d", y))
720       x = 9
721       while (x >= 1)
722         piece = @array[x][y]
723         if (piece)
724           s = piece.to_s
725         else
726           s = " * "
727         end
728         a.push(s)
729         x = x - 1
730       end
731       a.push(sprintf("\n"))
732       y = y + 1
733     end
734     if (! sente_hands.empty?)
735       a.push("P+")
736       sente_hands.each do |p|
737         a.push("00" + p.name)
738       end
739       a.push("\n")
740     end
741     if (! gote_hands.empty?)
742       a.push("P-")
743       gote_hands.each do |p|
744         a.push("00" + p.name)
745       end
746       a.push("\n")
747     end
748     a.push("%s\n" % [@teban ? "+" : "-"])
749     return a.join
750   end
751
752   # Return a CSA-styled string notation of the initial position.
753   #
754   def initial_string
755     tmp_board = self.class.new
756     tmp_board.initial
757     return tmp_board.to_s
758   end
759 end
760
761 end # ShogiServer