OSDN Git Service

Refactoring. Moved a method from the test case to board.rb so that the method is...
[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 module ShogiServer # for a namespace
21
22 class Board
23   def initialize(move_count=0)
24     @sente_hands = Array::new
25     @gote_hands  = Array::new
26     @history       = Hash::new(0)
27     @sente_history = Hash::new(0)
28     @gote_history  = Hash::new(0)
29     @array = [[], [], [], [], [], [], [], [], [], []]
30     @move_count = move_count
31     @teban = nil # black => true, white => false
32   end
33   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
34   attr_reader :move_count
35
36   def deep_copy
37     return Marshal.load(Marshal.dump(self))
38   end
39
40   def initial
41     PieceKY::new(self, 1, 1, false)
42     PieceKE::new(self, 2, 1, false)
43     PieceGI::new(self, 3, 1, false)
44     PieceKI::new(self, 4, 1, false)
45     PieceOU::new(self, 5, 1, false)
46     PieceKI::new(self, 6, 1, false)
47     PieceGI::new(self, 7, 1, false)
48     PieceKE::new(self, 8, 1, false)
49     PieceKY::new(self, 9, 1, false)
50     PieceKA::new(self, 2, 2, false)
51     PieceHI::new(self, 8, 2, false)
52     (1..9).each do |i|
53       PieceFU::new(self, i, 3, false)
54     end
55
56     PieceKY::new(self, 1, 9, true)
57     PieceKE::new(self, 2, 9, true)
58     PieceGI::new(self, 3, 9, true)
59     PieceKI::new(self, 4, 9, true)
60     PieceOU::new(self, 5, 9, true)
61     PieceKI::new(self, 6, 9, true)
62     PieceGI::new(self, 7, 9, true)
63     PieceKE::new(self, 8, 9, true)
64     PieceKY::new(self, 9, 9, true)
65     PieceKA::new(self, 8, 8, true)
66     PieceHI::new(self, 2, 8, true)
67     (1..9).each do |i|
68       PieceFU::new(self, i, 7, true)
69     end
70     @teban = true
71   end
72
73   def set_from_str(strs)
74     strs.split(/\n/).each do |str|
75       if (str =~ /^P\d/)
76         str.sub!(/^P(.)/, '')
77         y = $1.to_i
78         x = 9
79         while (str.length > 2)
80           str.sub!(/^(...?)/, '')
81           one = $1
82           if (one =~ /^([\+\-])(..)/)
83             sg = $1
84             name = $2
85             if (sg == "+")
86               sente = true
87             else
88               sente = false
89             end
90             if ((x < 1) || (9 < x) || (y < 1) || (9 < y))
91               raise "bad position #{x} #{y}"
92             end
93             case (name)
94             when "FU"
95               PieceFU::new(self, x, y, sente)
96             when "KY"
97               PieceKY::new(self, x, y, sente)
98             when "KE"
99               PieceKE::new(self, x, y, sente)
100             when "GI"
101               PieceGI::new(self, x, y, sente)
102             when "KI"
103               PieceKI::new(self, x, y, sente)
104             when "OU"
105               PieceOU::new(self, x, y, sente)
106             when "KA"
107               PieceKA::new(self, x, y, sente)
108             when "HI"
109               PieceHI::new(self, x, y, sente)
110             when "TO"
111               PieceFU::new(self, x, y, sente, true)
112             when "NY"
113               PieceKY::new(self, x, y, sente, true)
114             when "NK"
115               PieceKE::new(self, x, y, sente, true)
116             when "NG"
117               PieceGI::new(self, x, y, sente, true)
118             when "UM"
119               PieceKA::new(self, x, y, sente, true)
120             when "RY"
121               PieceHI::new(self, x, y, sente, true)
122             else
123               raise "unkown piece #{name}"
124             end
125           end
126           x = x - 1
127         end
128       elsif (str =~ /^P([\+\-])/)
129         sg = $1
130         if (sg == "+")
131           sente = true
132         else
133           sente = false
134         end
135         str.sub!(/^../, '')
136         while (str.length > 3)
137           str.sub!(/^..(..)/, '')
138           name = $1
139           case (name)
140           when "FU"
141             PieceFU::new(self, 0, 0, sente)
142           when "KY"
143             PieceKY::new(self, 0, 0, sente)
144           when "KE"
145             PieceKE::new(self, 0, 0, sente)
146           when "GI"
147             PieceGI::new(self, 0, 0, sente)
148           when "KI"
149             PieceKI::new(self, 0, 0, sente)
150           when "KA"
151             PieceKA::new(self, 0, 0, sente)
152           when "HI"
153             PieceHI::new(self, 0, 0, sente)
154           else
155             raise "unkown piece #{name}"
156           end
157         end # while
158       end # if
159     end # do
160   end
161
162   def have_piece?(hands, name)
163     piece = hands.find { |i|
164       i.name == name
165     }
166     return piece
167   end
168
169   def move_to(x0, y0, x1, y1, name, sente)
170     if (sente)
171       hands = @sente_hands
172     else
173       hands = @gote_hands
174     end
175
176     if ((x0 == 0) || (y0 == 0))
177       piece = have_piece?(hands, name)
178       return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
179       piece.move_to(x1, y1)
180     else
181       if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
182         return :illegal
183       end
184       if (@array[x0][y0].name != name) # promoted ?
185         @array[x0][y0].promoted = true
186       end
187       if (@array[x1][y1]) # capture
188         if (@array[x1][y1].name == "OU")
189           return :outori        # return board update
190         end
191         @array[x1][y1].sente = @array[x0][y0].sente
192         @array[x1][y1].move_to(0, 0)
193         hands.sort! {|a, b| # TODO refactor. Move to Piece class
194           a.name <=> b.name
195         }
196       end
197       @array[x0][y0].move_to(x1, y1)
198     end
199     @move_count += 1
200     @teban = @teban ? false : true
201     return true
202   end
203
204   def look_for_ou(sente)
205     x = 1
206     while (x <= 9)
207       y = 1
208       while (y <= 9)
209         if (@array[x][y] &&
210             (@array[x][y].name == "OU") &&
211             (@array[x][y].sente == sente))
212           return @array[x][y]
213         end
214         y = y + 1
215       end
216       x = x + 1
217     end
218     raise "can't find ou"
219   end
220
221   # note checkmate, but check. sente is checked.
222   def checkmated?(sente)        # sente is loosing
223     ou = look_for_ou(sente)
224     x = 1
225     while (x <= 9)
226       y = 1
227       while (y <= 9)
228         if (@array[x][y] &&
229             (@array[x][y].sente != sente))
230           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
231             return true
232           end
233         end
234         y = y + 1
235       end
236       x = x + 1
237     end
238     return false
239   end
240
241   def uchifuzume?(sente)
242     rival_ou = look_for_ou(! sente)   # rival's ou
243     if (sente)                  # rival is gote
244       if ((rival_ou.y != 9) &&
245           (@array[rival_ou.x][rival_ou.y + 1]) &&
246           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
247           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
248         fu_x = rival_ou.x
249         fu_y = rival_ou.y + 1
250       else
251         return false
252       end
253     else                        # gote
254       if ((rival_ou.y != 1) &&
255           (@array[rival_ou.x][rival_ou.y - 1]) &&
256           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
257           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
258         fu_x = rival_ou.x
259         fu_y = rival_ou.y - 1
260       else
261         return false
262       end
263     end
264
265     ## case: rival_ou is moving
266     rival_ou.movable_grids.each do |(cand_x, cand_y)|
267       tmp_board = deep_copy
268       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
269       raise "internal error" if (s != true)
270       if (! tmp_board.checkmated?(! sente)) # good move
271         return false
272       end
273     end
274
275     ## case: rival is capturing fu
276     x = 1
277     while (x <= 9)
278       y = 1
279       while (y <= 9)
280         if (@array[x][y] &&
281             (@array[x][y].sente != sente) &&
282             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
283           
284           names = []
285           if (@array[x][y].promoted)
286             names << @array[x][y].promoted_name
287           else
288             names << @array[x][y].name
289             if @array[x][y].promoted_name && 
290                @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
291               names << @array[x][y].promoted_name 
292             end
293           end
294           names.map! do |name|
295             tmp_board = deep_copy
296             s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
297             if s == :illegal
298               s # result
299             else
300               tmp_board.checkmated?(! sente) # result
301             end
302           end
303           all_illegal = names.find {|a| a != :illegal}
304           raise "internal error: legal move not found" if all_illegal == nil
305           r = names.find {|a| a == false} # good move
306           return false if r == false # found good move
307         end
308         y = y + 1
309       end
310       x = x + 1
311     end
312     return true
313   end
314
315   # @[sente|gote]_history has at least one item while the player is checking the other or 
316   # the other escapes.
317   def update_sennichite(player)
318     str = to_s
319     @history[str] += 1
320     if checkmated?(!player)
321       if (player)
322         @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
323       else
324         @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
325       end
326     else
327       if (player)
328         @sente_history.clear # no more continuous check
329       else
330         @gote_history.clear  # no more continuous check
331       end
332     end
333     if @sente_history.size > 0  # possible for Sente's or Gote's turn
334       @sente_history[str] += 1
335     end
336     if @gote_history.size > 0   # possible for Sente's or Gote's turn
337       @gote_history[str] += 1
338     end
339   end
340
341   def oute_sennichite?(player)
342     if (@sente_history[to_s] >= 4)
343       return :oute_sennichite_sente_lose
344     elsif (@gote_history[to_s] >= 4)
345       return :oute_sennichite_gote_lose
346     else
347       return nil
348     end
349   end
350
351   def sennichite?(sente)
352     if (@history[to_s] >= 4) # already 3 times
353       return true
354     end
355     return false
356   end
357
358   def good_kachi?(sente)
359     if (checkmated?(sente))
360       puts "'NG: Checkmating." if $DEBUG
361       return false 
362     end
363     
364     ou = look_for_ou(sente)
365     if (sente && (ou.y >= 4))
366       puts "'NG: Black's OU does not enter yet." if $DEBUG
367       return false     
368     end  
369     if (! sente && (ou.y <= 6))
370       puts "'NG: White's OU does not enter yet." if $DEBUG
371       return false 
372     end
373       
374     number = 0
375     point = 0
376
377     if (sente)
378       hands = @sente_hands
379       r = [1, 2, 3]
380     else
381       hands = @gote_hands
382       r = [7, 8, 9]
383     end
384     r.each do |y|
385       x = 1
386       while (x <= 9)
387         if (@array[x][y] &&
388             (@array[x][y].sente == sente) &&
389             (@array[x][y].point > 0))
390           point = point + @array[x][y].point
391           number = number + 1
392         end
393         x = x + 1
394       end
395     end
396     hands.each do |piece|
397       point = point + piece.point
398     end
399
400     if (number < 10)
401       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
402       return false     
403     end  
404     if (sente)
405       if (point < 28)
406         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
407         return false 
408       end  
409     else
410       if (point < 27)
411         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
412         return false 
413       end
414     end
415
416     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
417     return true
418   end
419
420   # sente is nil only if tests in test_board run
421   def handle_one_move(str, sente=nil)
422     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
423       sg = $1
424       x0 = $2.to_i
425       y0 = $3.to_i
426       x1 = $4.to_i
427       y1 = $5.to_i
428       name = $6
429     elsif (str =~ /^%KACHI/)
430       raise ArgumentError, "sente is null", caller if sente == nil
431       if (good_kachi?(sente))
432         return :kachi_win
433       else
434         return :kachi_lose
435       end
436     elsif (str =~ /^%TORYO/)
437       return :toryo
438     else
439       return :illegal
440     end
441     
442     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
443         ((x0 != 0) || (y0 != 0)))
444       return :illegal
445     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
446       return :illegal
447     end
448     
449     if (sg == "+")
450       sente = true if sente == nil           # deprecated
451       return :illegal unless sente == true   # black player's move must be black
452       hands = @sente_hands
453     else
454       sente = false if sente == nil          # deprecated
455       return :illegal unless sente == false  # white player's move must be white
456       hands = @gote_hands
457     end
458     
459     ## source check
460     if ((x0 == 0) && (y0 == 0))
461       return :illegal if (! have_piece?(hands, name))
462     elsif (! @array[x0][y0])
463       return :illegal           # no piece
464     elsif (@array[x0][y0].sente != sente)
465       return :illegal           # this is not mine
466     elsif (@array[x0][y0].name != name)
467       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
468     end
469
470     ## destination check
471     if (@array[x1][y1] &&
472         (@array[x1][y1].sente == sente)) # can't capture mine
473       return :illegal
474     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
475       return :illegal           # can't put on existing piece
476     end
477
478     tmp_board = deep_copy
479     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
480     return :oute_kaihimore if (tmp_board.checkmated?(sente))
481     tmp_board.update_sennichite(sente)
482     os_result = tmp_board.oute_sennichite?(sente)
483     return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
484     return :sennichite if tmp_board.sennichite?(sente)
485
486     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
487       return :uchifuzume
488     end
489
490     move_to(x0, y0, x1, y1, name, sente)
491
492     update_sennichite(sente)
493     return :normal
494   end
495
496   def to_s
497     a = Array::new
498     y = 1
499     while (y <= 9)
500       a.push(sprintf("P%d", y))
501       x = 9
502       while (x >= 1)
503         piece = @array[x][y]
504         if (piece)
505           s = piece.to_s
506         else
507           s = " * "
508         end
509         a.push(s)
510         x = x - 1
511       end
512       a.push(sprintf("\n"))
513       y = y + 1
514     end
515     if (! sente_hands.empty?)
516       a.push("P+")
517       sente_hands.each do |p|
518         a.push("00" + p.name)
519       end
520       a.push("\n")
521     end
522     if (! gote_hands.empty?)
523       a.push("P-")
524       gote_hands.each do |p|
525         a.push("00" + p.name)
526       end
527       a.push("\n")
528     end
529     a.push("%s\n" % [@teban ? "+" : "-"])
530     return a.join
531   end
532 end
533
534 end # ShogiServer