OSDN Git Service

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