OSDN Git Service

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