OSDN Git Service

Applied r247 changes for the test cases (spliting hte file).
[shogi-server/shogi-server.git] / shogi_server / game.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 GameResult
23   attr_reader :players, :black, :white
24
25   def initialize(game, p1, p2)
26     @game = game
27     @players = [p1, p2]
28     if p1.sente && !p2.sente
29       @black, @white = p1, p2
30     elsif !p1.sente && p2.sente
31       @black, @white = p2, p1
32     else
33       raise "Never reached!"
34     end
35     @players.each do |player|
36       player.status = "connected"
37       LEAGUE.save(player)
38     end
39   end
40
41   def process
42     raise "Implement me!"
43   end
44
45   def log(str)
46     @game.log_game(str)
47   end
48
49   def log_board
50     log(@game.board.to_s.gsub(/^/, "\'"))
51   end
52
53   def log_rating
54     log("'rating:%s\n" % [self.to_s]) if @game.rated?
55   end
56
57   def to_s
58     black_name = @black.rated? ? @black.player_id : @black.name
59     white_name = @white.rated? ? @white.player_id : @white.name
60     return "%s:%s" % [black_name, white_name]
61   end
62
63   def notify_monitor(type)
64     @game.each_monitor do |monitor|
65       monitor.write_safe(sprintf("##[MONITOR][%s] %s\n", @game.game_id, type))
66     end
67   end
68 end
69
70 class GameResultWin < GameResult
71   attr_reader :winner, :loser
72
73   def initialize(game, winner, loser)
74     super
75     @winner, @loser = winner, loser
76     @winner.last_game_win = true
77     @loser.last_game_win  = false
78   end
79
80   def log_summary(type)
81     log_board
82
83     black_result = white_result = ""
84     if @black == @winner
85       black_result = "win"
86       white_result = "lose"
87     else
88       black_result = "lose"
89       white_result = "win"
90     end
91     log("'summary:%s:%s %s:%s %s\n" % [type, 
92                                        @black.name, black_result,
93                                        @white.name, white_result])
94
95     log_rating
96   end
97 end
98
99 class GameResultAbnormalWin < GameResultWin
100   def process
101     @winner.write_safe("%TORYO\n#RESIGN\n#WIN\n")
102     @loser.write_safe( "%TORYO\n#RESIGN\n#LOSE\n")
103     log("%%TORYO\n")
104     log_summary("abnormal")
105     notify_monitor("%%TORYO")
106   end
107 end
108
109 class GameResultTimeoutWin < GameResultWin
110   def process
111     @winner.write_safe("#TIME_UP\n#WIN\n")
112     @loser.write_safe( "#TIME_UP\n#LOSE\n")
113     log_summary("time up")
114     notify_monitor("#TIME_UP")
115   end
116 end
117
118 # A player declares (successful) Kachi
119 class GameResultKachiWin < GameResultWin
120   def process
121     @winner.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
122     @loser.write_safe( "%KACHI\n#JISHOGI\n#LOSE\n")
123     log("%%KACHI\n")
124     log_summary("kachi")
125     notify_monitor("%%KACHI")
126   end
127 end
128
129 # A player declares wrong Kachi
130 class GameResultIllegalKachiWin < GameResultWin
131   def process
132     @winner.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
133     @loser.write_safe( "%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
134     log("%%KACHI\n")
135     log_summary("illegal kachi")
136     notify_monitor("%%KACHI")
137   end
138 end
139
140 class GameResultIllegalWin < GameResultWin
141   def initialize(game, winner, loser, cause)
142     super(game, winner, loser)
143     @cause = cause
144   end
145
146   def process
147     @winner.write_safe("#ILLEGAL_MOVE\n#WIN\n")
148     @loser.write_safe( "#ILLEGAL_MOVE\n#LOSE\n")
149     log_summary(@cause)
150     notify_monitor("#ILLEGAL_MOVE")
151   end
152 end
153
154 class GameResultIllegalMoveWin < GameResultIllegalWin
155   def initialize(game, winner, loser)
156     super(game, winner, loser, "illegal move")
157   end
158 end
159
160 class GameResultUchifuzumeWin < GameResultIllegalWin
161   def initialize(game, winner, loser)
162     super(game, winner, loser, "uchifuzume")
163   end
164 end
165
166 class GameResultOuteKaihiMoreWin < GameResultWin
167   def initialize(game, winner, loser)
168     super(game, winner, loser, "oute_kaihimore")
169   end
170 end
171
172 class GameResultOutoriWin < GameResultWin
173   def initialize(game, winner, loser)
174     super(game, winner, loser, "outori")
175   end
176 end
177
178 class GameReulstToryoWin < GameResultWin
179   def process
180     @winner.write_safe("%TORYO\n#RESIGN\n#WIN\n")
181     @loser.write_safe( "%TORYO\n#RESIGN\n#LOSE\n")
182     log("%%TORYO\n")
183     log_summary("toryo")
184     notify_monitor("%%TORYO")
185   end
186 end
187
188 class GameResultOuteSennichiteWin < GameResultWin
189   def process
190     @winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
191     @loser.write_safe( "#OUTE_SENNICHITE\n#LOSE\n")
192     log_summary("oute_sennichite")
193     notify_monitor("#OUTE_SENNICHITE")
194   end
195 end
196
197 class GameResultDraw < GameResult
198   def initialize(game, p1, p2)
199     super
200     p1.last_game_win = false
201     p2.last_game_win = false
202   end
203   
204   def log_summary(type)
205     log_board
206     log("'summary:%s:%s draw:%s draw\n", type, @black.name, @white.name)
207     log_rating
208   end
209 end
210
211 class GameResultSennichiteDraw < GameResultDraw
212   def process
213     @players.each do |player|
214       player.write_safe("#SENNICHITE\n#DRAW\n")
215     end
216     log_summary("sennichite")
217     notify_monitor("#SENNICHITE")
218   end
219 end
220
221 class Game
222   @@mutex = Mutex.new
223   @@time  = 0
224
225   def initialize(game_name, player0, player1)
226     @monitors = Array::new
227     @game_name = game_name
228     if (@game_name =~ /-(\d+)-(\d+)$/)
229       @total_time = $1.to_i
230       @byoyomi = $2.to_i
231     end
232
233     if (player0.sente)
234       @sente, @gote = player0, player1
235     else
236       @sente, @gote = player1, player0
237     end
238     @sente.socket_buffer.clear
239     @gote.socket_buffer.clear
240     @current_player, @next_player = @sente, @gote
241     @sente.game = self
242     @gote.game  = self
243
244     @last_move = ""
245     @current_turn = 0
246
247     @sente.status = "agree_waiting"
248     @gote.status  = "agree_waiting"
249
250     @game_id = sprintf("%s+%s+%s+%s+%s", 
251                   LEAGUE.event, @game_name, 
252                   @sente.name, @gote.name, issue_current_time)
253     
254     now = Time.now
255     log_dir_name = File.join(LEAGUE.dir, 
256                              now.strftime("%Y"),
257                              now.strftime("%m"),
258                              now.strftime("%d"))
259     FileUtils.mkdir_p(log_dir_name) unless File.exist?(log_dir_name)
260     @logfile = File.join(log_dir_name, @game_id + ".csa")
261
262     LEAGUE.games[@game_id] = self
263
264     log_message(sprintf("game created %s", @game_id))
265
266     @board = Board::new
267     @board.initial
268     @start_time = nil
269     @fh = open(@logfile, "w")
270     @fh.sync = true
271     @result = nil
272
273     propose
274   end
275   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors
276   attr_accessor :last_move, :current_turn
277   attr_reader   :result
278
279   def rated?
280     @sente.rated? && @gote.rated?
281   end
282
283   def turn?(player)
284     return player.status == "game" && @current_player == player
285   end
286
287   def monitoron(monitor)
288     @monitors.delete(monitor)
289     @monitors.push(monitor)
290   end
291
292   def monitoroff(monitor)
293     @monitors.delete(monitor)
294   end
295
296   def each_monitor
297     @monitors.each do |monitor|
298       yield monitor
299     end
300   end
301
302   def log_game(str)
303     if @fh.closed?
304       log_error("Failed to write to Game[%s]'s log file: %s" %
305                 [@game_id, str])
306     end
307     @fh.printf("%s\n", str)
308   end
309
310   def reject(rejector)
311     @sente.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
312     @gote.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
313     finish
314   end
315
316   def kill(killer)
317     if ["agree_waiting", "start_waiting"].include?(@sente.status)
318       reject(killer.name)
319     elsif (@current_player == killer)
320       result = GameResultAbnormalWin.new(self, @next_player, @current_player)
321       result.process
322       finish
323     end
324   end
325
326   def finish
327     log_message(sprintf("game finished %s", @game_id))
328     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
329     @fh.close
330
331     @sente.game = nil
332     @gote.game = nil
333     @sente.status = "connected"
334     @gote.status = "connected"
335
336     if (@current_player.protocol == LoginCSA::PROTOCOL)
337       @current_player.finish
338     end
339     if (@next_player.protocol == LoginCSA::PROTOCOL)
340       @next_player.finish
341     end
342     @monitors = Array::new
343     @sente = nil
344     @gote = nil
345     @current_player = nil
346     @next_player = nil
347     LEAGUE.games.delete(@game_id)
348   end
349
350   # class Game
351   def handle_one_move(str, player)
352     unless turn?(player)
353       return false if str == :timeout
354
355       @fh.puts("'Deferred %s" % [str])
356       log_warning("Deferred a move [%s] scince it is not %s 's turn." %
357                   [str, player.name])
358       player.socket_buffer << str # always in the player's thread
359       return nil
360     end
361
362     finish_flag = true
363     @end_time = Time::new
364     t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
365     
366     move_status = nil
367     if ((@current_player.mytime - t <= -@byoyomi) && 
368         ((@total_time > 0) || (@byoyomi > 0)))
369       status = :timeout
370     elsif (str == :timeout)
371       return false            # time isn't expired. players aren't swapped. continue game
372     else
373       @current_player.mytime -= t
374       if (@current_player.mytime < 0)
375         @current_player.mytime = 0
376       end
377
378       move_status = @board.handle_one_move(str, @sente == @current_player)
379
380       if [:illegal, :uchifuzume, :oute_kaihimore].include?(move_status)
381         @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
382       else
383         if [:normal, :outori, :sennichite, :oute_sennichite_sente_lose, :oute_sennichite_gote_lose].include?(move_status)
384           # Thinking time includes network traffic
385           @sente.write_safe(sprintf("%s,T%d\n", str, t))
386           @gote.write_safe(sprintf("%s,T%d\n", str, t))
387           @fh.printf("%s\nT%d\n", str, t)
388           @last_move = sprintf("%s,T%d", str, t)
389           @current_turn += 1
390         end
391
392         @monitors.each do |monitor|
393           monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@game_id}] "))
394           monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @game_id))
395         end
396       end
397     end
398
399     result = nil
400     if (@next_player.status != "game") # rival is logout or disconnected
401       result = GameResultAbnormalWin.new(self, @current_player, @next_player)
402     elsif (status == :timeout)
403       # current_player losed
404       result = GameResultTimeoutWin.new(self, @next_player, @current_player)
405     elsif (move_status == :illegal)
406       result = GameResultIllegalMoveWin.new(self, @next_player, @current_player)
407     elsif (move_status == :kachi_win)
408       result = GameResultKachiWin.new(self, @current_player, @next_player)
409     elsif (move_status == :kachi_lose)
410       result = GameResultIllegalKachiWin.new(self, @next_player, @current_player)
411     elsif (move_status == :toryo)
412       result = GameReulstToryoWin.new(self, @next_player, @current_player)
413     elsif (move_status == :outori)
414       # The current player captures the next player's king
415       result = GameResultOutoriWin.new(self, @current_player, @next_player)
416     elsif (move_status == :oute_sennichite_sente_lose)
417       result = GameResultOuteSennichiteWin.new(self, @gote, @sente) # Sente is checking
418     elsif (move_status == :oute_sennichite_gote_lose)
419       result = GameResultOuteSennichiteWin.new(self, @sente, @gote) # Gote is checking
420     elsif (move_status == :sennichite)
421       result = GameResultSennichiteDraw.new(self, @current_player, @next_player)
422     elsif (move_status == :uchifuzume)
423       # the current player losed
424       result = GameResultUchifuzumeWin.new(self, @next_player, @current_player)
425     elsif (move_status == :oute_kaihimore)
426       # the current player losed
427       result = GameResultOuteKaihiMoreWin.new(self, @next_player, @current_player)
428     else
429       finish_flag = false
430     end
431     result.process if result
432     finish() if finish_flag
433     @current_player, @next_player = @next_player, @current_player
434     @start_time = Time::new
435     return finish_flag
436   end
437
438   def start
439     log_message(sprintf("game started %s", @game_id))
440     @sente.write_safe(sprintf("START:%s\n", @game_id))
441     @gote.write_safe(sprintf("START:%s\n", @game_id))
442     @sente.mytime = @total_time
443     @gote.mytime = @total_time
444     @start_time = Time::new
445   end
446
447   def propose
448     @fh.puts("V2")
449     @fh.puts("N+#{@sente.name}")
450     @fh.puts("N-#{@gote.name}")
451     @fh.puts("$EVENT:#{@game_id}")
452
453     @sente.write_safe(propose_message("+"))
454     @gote.write_safe(propose_message("-"))
455
456     now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
457     @fh.puts("$START_TIME:#{now}")
458     @fh.print <<EOM
459 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
460 P2 * -HI *  *  *  *  * -KA * 
461 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
462 P4 *  *  *  *  *  *  *  *  * 
463 P5 *  *  *  *  *  *  *  *  * 
464 P6 *  *  *  *  *  *  *  *  * 
465 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
466 P8 * +KA *  *  *  *  * +HI * 
467 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
468 +
469 EOM
470   end
471
472   def show()
473     str0 = <<EOM
474 BEGIN Game_Summary
475 Protocol_Version:1.1
476 Protocol_Mode:Server
477 Format:Shogi 1.0
478 Declaration:Jishogi 1.1
479 Game_ID:#{@game_id}
480 Name+:#{@sente.name}
481 Name-:#{@gote.name}
482 Rematch_On_Draw:NO
483 To_Move:+
484 BEGIN Time
485 Time_Unit:1sec
486 Total_Time:#{@total_time}
487 Byoyomi:#{@byoyomi}
488 Least_Time_Per_Move:#{Least_Time_Per_Move}
489 Remaining_Time+:#{@sente.mytime}
490 Remaining_Time-:#{@gote.mytime}
491 Last_Move:#{@last_move}
492 Current_Turn:#{@current_turn}
493 END Time
494 BEGIN Position
495 EOM
496
497     str1 = <<EOM
498 END Position
499 END Game_Summary
500 EOM
501
502     return str0 + @board.to_s + str1
503   end
504
505   def propose_message(sg_flag)
506     str = <<EOM
507 BEGIN Game_Summary
508 Protocol_Version:1.1
509 Protocol_Mode:Server
510 Format:Shogi 1.0
511 Declaration:Jishogi 1.1
512 Game_ID:#{@game_id}
513 Name+:#{@sente.name}
514 Name-:#{@gote.name}
515 Your_Turn:#{sg_flag}
516 Rematch_On_Draw:NO
517 To_Move:+
518 BEGIN Time
519 Time_Unit:1sec
520 Total_Time:#{@total_time}
521 Byoyomi:#{@byoyomi}
522 Least_Time_Per_Move:#{Least_Time_Per_Move}
523 END Time
524 BEGIN Position
525 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
526 P2 * -HI *  *  *  *  * -KA * 
527 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
528 P4 *  *  *  *  *  *  *  *  * 
529 P5 *  *  *  *  *  *  *  *  * 
530 P6 *  *  *  *  *  *  *  *  * 
531 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
532 P8 * +KA *  *  *  *  * +HI * 
533 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
534 P+
535 P-
536 +
537 END Position
538 END Game_Summary
539 EOM
540     return str
541   end
542   
543   private
544   
545   def issue_current_time
546     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
547     @@mutex.synchronize do
548       while time <= @@time do
549         time += 1
550       end
551       @@time = time
552     end
553   end
554 end
555
556 end # ShogiServer