OSDN Git Service

5f7cd0eaea9da4029a963aaf01699ec9a3fd90aa
[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 require 'shogi_server/league/floodgate'
21 require 'shogi_server/game_result'
22 require 'shogi_server/util'
23
24 module ShogiServer # for a namespace
25
26 class Game
27   # When this duration passes after this object instanciated (i.e.
28   # the agree_waiting or start_waiting state lasts too long),
29   # the game will be rejected by the Server.
30   WAITING_EXPIRATION = 120 # seconds
31
32   @@mutex = Mutex.new
33   @@time  = 0
34   def initialize(game_name, player0, player1, board)
35     @monitors = Array::new # array of MonitorHandler*
36     @game_name = game_name
37     if (@game_name =~ /-(\d+)-(\d+)$/)
38       @total_time = $1.to_i
39       @byoyomi = $2.to_i
40     end
41
42     if (player0.sente)
43       @sente, @gote = player0, player1
44     else
45       @sente, @gote = player1, player0
46     end
47     @sente.socket_buffer.clear
48     @gote.socket_buffer.clear
49     @board = board
50     if @board.teban
51       @current_player, @next_player = @sente, @gote
52     else
53       @current_player, @next_player = @gote, @sente
54     end
55     @sente.game = self
56     @gote.game  = self
57
58     @last_move = @board.initial_moves.empty? ? "" : "%s,T1" % [@board.initial_moves.last]
59     @current_turn = @board.initial_moves.size
60
61     @sente.status = "agree_waiting"
62     @gote.status  = "agree_waiting"
63
64     @game_id = sprintf("%s+%s+%s+%s+%s", 
65                   $league.event, @game_name, 
66                   @sente.name, @gote.name, issue_current_time)
67     
68     # The time when this Game instance was created.
69     # Don't be confused with @start_time when the game was started to play.
70     @prepared_time = Time.now 
71     log_dir_name = File.join($league.dir, 
72                              @prepared_time.strftime("%Y"),
73                              @prepared_time.strftime("%m"),
74                              @prepared_time.strftime("%d"))
75     @logfile = File.join(log_dir_name, @game_id + ".csa")
76     Mkdir.mkdir_for(@logfile)
77
78     $league.games[@game_id] = self
79
80     log_message(sprintf("game created %s", @game_id))
81
82     @start_time = nil
83     @fh = open(@logfile, "w")
84     @fh.sync = true
85     @result = nil
86
87     propose
88   end
89   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors
90   attr_accessor :last_move, :current_turn
91   attr_reader   :result, :prepared_time
92
93   # Path of a log file for this game.
94   attr_reader   :logfile
95
96   def rated?
97     @sente.rated? && @gote.rated?
98   end
99
100   def turn?(player)
101     return player.status == "game" && @current_player == player
102   end
103
104   def monitoron(monitor_handler)
105     monitoroff(monitor_handler)
106     @monitors.push(monitor_handler)
107   end
108
109   def monitoroff(monitor_handler)
110     @monitors.delete_if {|mon| mon == monitor_handler}
111   end
112
113   def each_monitor
114     @monitors.each do |monitor_handler|
115       yield monitor_handler
116     end
117   end
118
119   def log_game(str)
120     if @fh.closed?
121       log_error("Failed to write to Game[%s]'s log file: %s" %
122                 [@game_id, str])
123     end
124     @fh.printf("%s\n", str)
125   end
126
127   def reject(rejector)
128     @sente.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
129     @gote.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
130     finish
131   end
132
133   def kill(killer)
134     [@sente, @gote].each do |player|
135       if ["agree_waiting", "start_waiting"].include?(player.status)
136         reject(killer.name)
137         return # return from this method
138       end
139     end
140     
141     if (@current_player == killer)
142       @result = GameResultAbnormalWin.new(self, @next_player, @current_player)
143       @result.process
144       finish
145     end
146   end
147
148   def finish
149     log_message(sprintf("game finished %s", @game_id))
150
151     # In a case where a player in agree_waiting or start_waiting status is
152     # rejected, a GameResult object is not yet instanciated.
153     # See test/TC_before_agree.rb.
154     end_time = @result ? @result.end_time : Time.now
155     @fh.printf("'$END_TIME:%s\n", end_time.strftime("%Y/%m/%d %H:%M:%S"))    
156     @fh.close
157
158     @sente.game = nil
159     @gote.game = nil
160     @sente.status = "connected"
161     @gote.status = "connected"
162
163     if (@current_player.protocol == LoginCSA::PROTOCOL)
164       @current_player.finish
165     end
166     if (@next_player.protocol == LoginCSA::PROTOCOL)
167       @next_player.finish
168     end
169     @monitors = Array::new
170     @sente = nil
171     @gote = nil
172     @current_player = nil
173     @next_player = nil
174     $league.games.delete(@game_id)
175   end
176
177   # class Game
178   def handle_one_move(str, player, end_time)
179     unless turn?(player)
180       return false if str == :timeout
181
182       @fh.puts("'Deferred %s" % [str])
183       log_warning("Deferred a move [%s] scince it is not %s 's turn." %
184                   [str, player.name])
185       player.socket_buffer << str # always in the player's thread
186       return nil
187     end
188
189     finish_flag = true
190     @end_time = end_time
191     t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
192     
193     move_status = nil
194     if ((@current_player.mytime - t <= -@byoyomi) && 
195         ((@total_time > 0) || (@byoyomi > 0)))
196       status = :timeout
197     elsif (str == :timeout)
198       return false            # time isn't expired. players aren't swapped. continue game
199     else
200       @current_player.mytime -= t
201       if (@current_player.mytime < 0)
202         @current_player.mytime = 0
203       end
204
205       move_status = @board.handle_one_move(str, @sente == @current_player)
206       # log_debug("move_status: %s for %s's %s" % [move_status, @sente == @current_player ? "BLACK" : "WHITE", str])
207
208       if [:illegal, :uchifuzume, :oute_kaihimore].include?(move_status)
209         @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
210       else
211         if :toryo != move_status
212           # Thinking time includes network traffic
213           @sente.write_safe(sprintf("%s,T%d\n", str, t))
214           @gote.write_safe(sprintf("%s,T%d\n", str, t))
215           @fh.printf("%s\nT%d\n", str, t)
216           @last_move = sprintf("%s,T%d", str, t)
217           @current_turn += 1
218
219           @monitors.each do |monitor_handler|
220             monitor_handler.write_one_move(@game_id, self)
221           end
222         end # if
223         # if move_status is :toryo then a GameResult message will be sent to monitors   
224       end # if
225     end
226
227     @result = nil
228     if (@next_player.status != "game") # rival is logout or disconnected
229       @result = GameResultAbnormalWin.new(self, @current_player, @next_player)
230     elsif (status == :timeout)
231       # current_player losed
232       @result = GameResultTimeoutWin.new(self, @next_player, @current_player)
233     elsif (move_status == :illegal)
234       @result = GameResultIllegalMoveWin.new(self, @next_player, @current_player)
235     elsif (move_status == :kachi_win)
236       @result = GameResultKachiWin.new(self, @current_player, @next_player)
237     elsif (move_status == :kachi_lose)
238       @result = GameResultIllegalKachiWin.new(self, @next_player, @current_player)
239     elsif (move_status == :toryo)
240       @result = GameResultToryoWin.new(self, @next_player, @current_player)
241     elsif (move_status == :outori)
242       # The current player captures the next player's king
243       @result = GameResultOutoriWin.new(self, @current_player, @next_player)
244     elsif (move_status == :oute_sennichite_sente_lose)
245       @result = GameResultOuteSennichiteWin.new(self, @gote, @sente) # Sente is checking
246     elsif (move_status == :oute_sennichite_gote_lose)
247       @result = GameResultOuteSennichiteWin.new(self, @sente, @gote) # Gote is checking
248     elsif (move_status == :sennichite)
249       @result = GameResultSennichiteDraw.new(self, @current_player, @next_player)
250     elsif (move_status == :uchifuzume)
251       # the current player losed
252       @result = GameResultUchifuzumeWin.new(self, @next_player, @current_player)
253     elsif (move_status == :oute_kaihimore)
254       # the current player losed
255       @result = GameResultOuteKaihiMoreWin.new(self, @next_player, @current_player)
256     else
257       finish_flag = false
258     end
259     @result.process if @result
260     finish() if finish_flag
261     @current_player, @next_player = @next_player, @current_player
262     @start_time = Time.now
263     return finish_flag
264   end
265
266   def is_startable_status?
267     return (@sente && @gote &&
268             (@sente.status == "start_waiting") &&
269             (@gote.status  == "start_waiting"))
270   end
271
272   def start
273     log_message(sprintf("game started %s", @game_id))
274     @sente.status = "game"
275     @gote.status  = "game"
276     @sente.write_safe(sprintf("START:%s\n", @game_id))
277     @gote.write_safe(sprintf("START:%s\n", @game_id))
278     @sente.mytime = @total_time
279     @gote.mytime = @total_time
280     @start_time = Time.now
281   end
282
283   def propose
284     @fh.puts("V2")
285     @fh.puts("N+#{@sente.name}")
286     @fh.puts("N-#{@gote.name}")
287     @fh.puts("$EVENT:#{@game_id}")
288
289     @sente.write_safe(propose_message("+"))
290     @gote.write_safe(propose_message("-"))
291
292     now = Time.now.strftime("%Y/%m/%d %H:%M:%S")
293     @fh.puts("$START_TIME:#{now}")
294     @fh.print <<EOM
295 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
296 P2 * -HI *  *  *  *  * -KA * 
297 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
298 P4 *  *  *  *  *  *  *  *  * 
299 P5 *  *  *  *  *  *  *  *  * 
300 P6 *  *  *  *  *  *  *  *  * 
301 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
302 P8 * +KA *  *  *  *  * +HI * 
303 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
304 +
305 EOM
306     if rated?
307       black_name = @sente.rated? ? @sente.player_id : @sente.name
308       white_name = @gote.rated?  ? @gote.player_id  : @gote.name
309       @fh.puts("'rating:%s:%s" % [black_name, white_name])
310     end
311     unless @board.initial_moves.empty?
312       @fh.puts "'buoy game starting with %d moves" % [@board.initial_moves.size]
313       @board.initial_moves.each do |move|
314         @fh.puts move
315         @fh.puts "T1"
316       end
317     end
318   end
319
320   def show()
321     str0 = <<EOM
322 BEGIN Game_Summary
323 Protocol_Version:1.1
324 Protocol_Mode:Server
325 Format:Shogi 1.0
326 Declaration:Jishogi 1.1
327 Game_ID:#{@game_id}
328 Name+:#{@sente.name}
329 Name-:#{@gote.name}
330 Rematch_On_Draw:NO
331 To_Move:+
332 BEGIN Time
333 Time_Unit:1sec
334 Total_Time:#{@total_time}
335 Byoyomi:#{@byoyomi}
336 Least_Time_Per_Move:#{Least_Time_Per_Move}
337 Remaining_Time+:#{@sente.mytime}
338 Remaining_Time-:#{@gote.mytime}
339 Last_Move:#{@last_move}
340 Current_Turn:#{@current_turn}
341 END Time
342 BEGIN Position
343 EOM
344
345     str1 = <<EOM
346 END Position
347 END Game_Summary
348 EOM
349
350     return str0 + @board.to_s + str1
351   end
352
353   def propose_message(sg_flag)
354     str = <<EOM
355 BEGIN Game_Summary
356 Protocol_Version:1.1
357 Protocol_Mode:Server
358 Format:Shogi 1.0
359 Declaration:Jishogi 1.1
360 Game_ID:#{@game_id}
361 Name+:#{@sente.name}
362 Name-:#{@gote.name}
363 Your_Turn:#{sg_flag}
364 Rematch_On_Draw:NO
365 To_Move:#{@board.teban ? "+" : "-"}
366 BEGIN Time
367 Time_Unit:1sec
368 Total_Time:#{@total_time}
369 Byoyomi:#{@byoyomi}
370 Least_Time_Per_Move:#{Least_Time_Per_Move}
371 END Time
372 BEGIN Position
373 #{Board::INITIAL_POSITION}
374 #{@board.initial_moves.collect {|m| m + ",T1"}.join("\n")}
375 END Position
376 END Game_Summary
377 EOM
378     # An empty @board.initial_moves causes an empty line, which should be
379     # eliminated.
380     return str.gsub("\n\n", "\n")
381   end
382
383   def prepared_expire?
384     if @prepared_time && (@prepared_time + WAITING_EXPIRATION < Time.now)
385       return true
386     end
387
388     return false
389   end
390   
391   private
392   
393   def issue_current_time
394     time = Time.now.strftime("%Y%m%d%H%M%S").to_i
395     @@mutex.synchronize do
396       while time <= @@time do
397         time += 1
398       end
399       @@time = time
400     end
401   end
402 end
403
404 end # ShogiServer