OSDN Git Service

* [shogi-server]
[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 notify_monitor(type)
54     @game.each_monitor do |monitor|
55       monitor.write_safe(sprintf("##[MONITOR][%s] %s\n", @game.game_id, type))
56     end
57   end
58 end
59
60 class GameResultWin < GameResult
61   attr_reader :winner, :loser
62
63   def initialize(game, winner, loser)
64     super
65     @winner, @loser = winner, loser
66     @winner.last_game_win = true
67     @loser.last_game_win  = false
68   end
69
70   def log_summary(type)
71     log_board
72
73     black_result = white_result = ""
74     if @black == @winner
75       black_result = "win"
76       white_result = "lose"
77     else
78       black_result = "lose"
79       white_result = "win"
80     end
81     log("'summary:%s:%s %s:%s %s\n" % [type, 
82                                        @black.name, black_result,
83                                        @white.name, white_result])
84
85   end
86 end
87
88 class GameResultAbnormalWin < GameResultWin
89   def process
90     @winner.write_safe("%TORYO\n#RESIGN\n#WIN\n")
91     @loser.write_safe( "%TORYO\n#RESIGN\n#LOSE\n")
92     log("%%TORYO\n")
93     log_summary("abnormal")
94     notify_monitor("%%TORYO")
95   end
96 end
97
98 class GameResultTimeoutWin < GameResultWin
99   def process
100     @winner.write_safe("#TIME_UP\n#WIN\n")
101     @loser.write_safe( "#TIME_UP\n#LOSE\n")
102     log_summary("time up")
103     notify_monitor("#TIME_UP")
104   end
105 end
106
107 # A player declares (successful) Kachi
108 class GameResultKachiWin < GameResultWin
109   def process
110     @winner.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
111     @loser.write_safe( "%KACHI\n#JISHOGI\n#LOSE\n")
112     log("%%KACHI\n")
113     log_summary("kachi")
114     notify_monitor("%%KACHI")
115   end
116 end
117
118 # A player declares wrong Kachi
119 class GameResultIllegalKachiWin < GameResultWin
120   def process
121     @winner.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
122     @loser.write_safe( "%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
123     log("%%KACHI\n")
124     log_summary("illegal kachi")
125     notify_monitor("%%KACHI")
126   end
127 end
128
129 class GameResultIllegalWin < GameResultWin
130   def initialize(game, winner, loser, cause)
131     super(game, winner, loser)
132     @cause = cause
133   end
134
135   def process
136     @winner.write_safe("#ILLEGAL_MOVE\n#WIN\n")
137     @loser.write_safe( "#ILLEGAL_MOVE\n#LOSE\n")
138     log_summary(@cause)
139     notify_monitor("#ILLEGAL_MOVE")
140   end
141 end
142
143 class GameResultIllegalMoveWin < GameResultIllegalWin
144   def initialize(game, winner, loser)
145     super(game, winner, loser, "illegal move")
146   end
147 end
148
149 class GameResultUchifuzumeWin < GameResultIllegalWin
150   def initialize(game, winner, loser)
151     super(game, winner, loser, "uchifuzume")
152   end
153 end
154
155 class GameResultOuteKaihiMoreWin < GameResultWin
156   def initialize(game, winner, loser)
157     super(game, winner, loser, "oute_kaihimore")
158   end
159 end
160
161 class GameResultOutoriWin < GameResultWin
162   def initialize(game, winner, loser)
163     super(game, winner, loser, "outori")
164   end
165 end
166
167 class GameReulstToryoWin < GameResultWin
168   def process
169     @winner.write_safe("%TORYO\n#RESIGN\n#WIN\n")
170     @loser.write_safe( "%TORYO\n#RESIGN\n#LOSE\n")
171     log("%%TORYO\n")
172     log_summary("toryo")
173     notify_monitor("%%TORYO")
174   end
175 end
176
177 class GameResultOuteSennichiteWin < GameResultWin
178   def process
179     @winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
180     @loser.write_safe( "#OUTE_SENNICHITE\n#LOSE\n")
181     log_summary("oute_sennichite")
182     notify_monitor("#OUTE_SENNICHITE")
183   end
184 end
185
186 class GameResultDraw < GameResult
187   def initialize(game, p1, p2)
188     super
189     p1.last_game_win = false
190     p2.last_game_win = false
191   end
192   
193   def log_summary(type)
194     log_board
195     log("'summary:%s:%s draw:%s draw\n", type, @black.name, @white.name)
196   end
197 end
198
199 class GameResultSennichiteDraw < GameResultDraw
200   def process
201     @players.each do |player|
202       player.write_safe("#SENNICHITE\n#DRAW\n")
203     end
204     log_summary("sennichite")
205     notify_monitor("#SENNICHITE")
206   end
207 end
208
209 class Game
210   @@mutex = Mutex.new
211   @@time  = 0
212
213   def initialize(game_name, player0, player1)
214     @monitors = Array::new
215     @game_name = game_name
216     if (@game_name =~ /-(\d+)-(\d+)$/)
217       @total_time = $1.to_i
218       @byoyomi = $2.to_i
219     end
220
221     if (player0.sente)
222       @sente, @gote = player0, player1
223     else
224       @sente, @gote = player1, player0
225     end
226     @sente.socket_buffer.clear
227     @gote.socket_buffer.clear
228     @current_player, @next_player = @sente, @gote
229     @sente.game = self
230     @gote.game  = self
231
232     @last_move = ""
233     @current_turn = 0
234
235     @sente.status = "agree_waiting"
236     @gote.status  = "agree_waiting"
237
238     @game_id = sprintf("%s+%s+%s+%s+%s", 
239                   LEAGUE.event, @game_name, 
240                   @sente.name, @gote.name, issue_current_time)
241     
242     now = Time.now
243     log_dir_name = File.join(LEAGUE.dir, 
244                              now.strftime("%Y"),
245                              now.strftime("%m"),
246                              now.strftime("%d"))
247     FileUtils.mkdir_p(log_dir_name) unless File.exist?(log_dir_name)
248     @logfile = File.join(log_dir_name, @game_id + ".csa")
249
250     LEAGUE.games[@game_id] = self
251
252     log_message(sprintf("game created %s", @game_id))
253
254     @board = Board::new
255     @board.initial
256     @start_time = nil
257     @fh = open(@logfile, "w")
258     @fh.sync = true
259     @result = nil
260
261     propose
262   end
263   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors
264   attr_accessor :last_move, :current_turn
265   attr_reader   :result
266
267   def rated?
268     @sente.rated? && @gote.rated?
269   end
270
271   def turn?(player)
272     return player.status == "game" && @current_player == player
273   end
274
275   def monitoron(monitor)
276     @monitors.delete(monitor)
277     @monitors.push(monitor)
278   end
279
280   def monitoroff(monitor)
281     @monitors.delete(monitor)
282   end
283
284   def each_monitor
285     @monitors.each do |monitor|
286       yield monitor
287     end
288   end
289
290   def log_game(str)
291     if @fh.closed?
292       log_error("Failed to write to Game[%s]'s log file: %s" %
293                 [@game_id, str])
294     end
295     @fh.printf("%s\n", str)
296   end
297
298   def reject(rejector)
299     @sente.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
300     @gote.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
301     finish
302   end
303
304   def kill(killer)
305     if ["agree_waiting", "start_waiting"].include?(@sente.status)
306       reject(killer.name)
307     elsif (@current_player == killer)
308       result = GameResultAbnormalWin.new(self, @next_player, @current_player)
309       result.process
310       finish
311     end
312   end
313
314   def finish
315     log_message(sprintf("game finished %s", @game_id))
316     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
317     @fh.close
318
319     @sente.game = nil
320     @gote.game = nil
321     @sente.status = "connected"
322     @gote.status = "connected"
323
324     if (@current_player.protocol == LoginCSA::PROTOCOL)
325       @current_player.finish
326     end
327     if (@next_player.protocol == LoginCSA::PROTOCOL)
328       @next_player.finish
329     end
330     @monitors = Array::new
331     @sente = nil
332     @gote = nil
333     @current_player = nil
334     @next_player = nil
335     LEAGUE.games.delete(@game_id)
336   end
337
338   # class Game
339   def handle_one_move(str, player)
340     unless turn?(player)
341       return false if str == :timeout
342
343       @fh.puts("'Deferred %s" % [str])
344       log_warning("Deferred a move [%s] scince it is not %s 's turn." %
345                   [str, player.name])
346       player.socket_buffer << str # always in the player's thread
347       return nil
348     end
349
350     finish_flag = true
351     @end_time = Time::new
352     t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
353     
354     move_status = nil
355     if ((@current_player.mytime - t <= -@byoyomi) && 
356         ((@total_time > 0) || (@byoyomi > 0)))
357       status = :timeout
358     elsif (str == :timeout)
359       return false            # time isn't expired. players aren't swapped. continue game
360     else
361       @current_player.mytime -= t
362       if (@current_player.mytime < 0)
363         @current_player.mytime = 0
364       end
365
366       move_status = @board.handle_one_move(str, @sente == @current_player)
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