OSDN Git Service

need to update message from server to client
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## -*-Ruby-*- $RCSfile$ $Revision$ $Name$
3
4 ## Copyright (C) 2004 773@2ch
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 DEFAULT_TIMEOUT = 10            # for single socket operation
21 Total_Time = 1500
22 Least_Time_Per_Move = 1
23 Watchdog_Time = 30              # time for ping
24 Login_Time = 300                # time for LOGIN
25
26 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
27 Release.concat("-") if (Release == "")
28 Revision = "$Revision$".gsub(/[^\.\d]/, '')
29
30 STDOUT.sync = true
31 STDERR.sync = true
32
33 require 'getoptlong'
34 require 'thread'
35 require 'timeout'
36 require 'socket'
37 require 'ping'
38
39 TCPSocket.do_not_reverse_lookup = true
40
41 class TCPSocket
42   def gets_timeout(t = DEFAULT_TIMEOUT)
43     begin
44       timeout(t) do
45         return self.gets
46       end
47     rescue TimeoutError
48       return nil
49     rescue
50       return nil
51     end
52   end
53   def gets_safe
54     begin
55       return self.gets
56     rescue
57       return nil
58     end
59   end
60   def write_safe(str)
61     begin
62       return self.write(str)
63     rescue
64       return nil
65     end
66   end
67 end
68
69
70 class League
71   def initialize
72     @hash = Hash::new
73   end
74   attr_accessor :hash
75
76   def add(player)
77     @hash[player.name] = player
78   end
79   def delete(player)
80     @hash.delete(player.name)
81   end
82   def duplicated?(player)
83     if (@hash[player.name])
84       return true
85     else
86       return false
87     end
88   end
89   def get_player(status, game_name, sente)
90     @hash.each do |name, player|
91       if ((player.status == status)
92           (player.game_name == game_name)
93           (player.sente == sente))
94         return player
95       end
96     end
97     return nil
98   end
99   def new_game(game_name, player0, player1)
100     game = Game::new(game_name, player0, player1)
101   end
102 end
103
104
105
106
107 class Player
108   def initialize(str, socket)
109     @name = nil
110     @password = nil
111     @socket = socket
112     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game
113
114     @x1 = false                 # extention protocol
115     @eol = "\m"                 # favorite eol code
116     @game = nil
117     @game_name = ""
118     @mytime = Total_Time
119     @sente = nil
120     @watchdog_thread = nil
121
122     login(str)
123   end
124
125   attr_accessor :name, :password, :socket, :status
126   attr_accessor :x1, :eol, :game, :mytime, :watchdog_thread, :game_name, :sente
127
128   def finish
129     Thread::kill(@watchdog_thread) if @watchdog_thread
130     @socket.close
131   end
132
133   def watchdog(time)
134     while true
135       begin
136         Ping.pingecho(@socket.addr[3])
137       rescue
138       end
139       sleep(time)
140     end
141   end
142
143   def to_s
144     if ((status == "game_waiting") ||
145         (status == "agree_waiting") ||
146         (status == "game"))
147       if (@sente)
148         return sprintf("%s %s %s +", @name, @status, @game_name, "+")
149       else
150         return sprintf("%s %s %s -", @name, @status, @game_name, "-")
151       end
152     else
153       return sprintf("%s %s", @name, @status)
154     end
155   end
156
157   def write_help(str)
158     @socket.write_safe('## available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
159   end
160
161   def write_safe(str)
162     @socket.write_safe(str.gsub(/[\r\n]+/, @eol))
163   end
164
165   def login(str)
166     str =~ /([\r\n]*)$/
167     @eol = $1
168     str.chomp!
169     (login, @name, @password, ext) = str.split
170     @x1 = true if (ext)
171     @watchdog_thread = Thread::start do
172       watchdog(Watchdog_Time)
173     end
174   end
175     
176   def run
177     if (@x1)
178       log_message(sprintf("user %s run in x1 mode", @name))
179       write_safe("## LOGIN in x1 mode\n")
180     else
181       log_message(sprintf("user %s run in CSA mode", @name))
182     end
183
184     while (str = @socket.gets_safe)
185       str.chomp!
186       case str
187       when /^[\+\-%][^%]/
188         if (@status == "game")
189           @game.handle_one_move(str, self)
190         else
191           write_safe("## you are in %s status. %s in game status\n", @status)
192           next
193         end
194       when /^AGREE/
195         if (@status == "agree_waiting")
196           @status = "start_waiting"
197           if ((@game.sente.status == "start_waiting") &&
198               (@game.gote.status == "start_waiting"))
199             @game.start
200             @game.sente.status = "game"
201             @game.gote.status = "game"
202           end
203         else
204           write_safe("## you are in %s status. AGREE is valid in agree_waiting status\n", @status)
205           next
206         end
207       when /^%%HELP/
208         write_help
209       when /^%%GAME\s+(\S+)\s+([\+\-])/
210         if ((@status == "connected") || (@status == "game_waiting"))
211           @status = "game_waiting"
212         else
213           write_safe("## you are in %s status. GAME is valid in connected or game_waiting status\n", @status)
214           next
215         end
216         @status = "game_waiting"
217         @game_name = $1
218         if ($2 == "+")
219           @sente = true
220           rival_sente = false
221         else
222           @sente = false
223           rival_sente = true
224         end
225         rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente)
226         if (rival)
227           LEAGUE.new_game(@game_name, self, rival)
228           self.status = "agree_waiting"
229           rival.status = "agree_waiting"
230         end
231       when /^%%CHAT\s+(\S+)/
232         message = $1
233         LEAGUE.hash.each do |name, player|
234           s = player.write_safe(sprintf("## [%s] %s\n", @name, message))
235           player.status = "zombie" if (! s)
236         end
237       when /^%%WHO/
238         LEAGUE.hash.each do |name, player|
239           write_safe(sprintf("## %s\n", player.to_s))
240         end
241       when /^%%LOGOUT/
242         break
243       else
244         write_safe(sprintf("## unknown command %s\n", str))
245       end
246     end
247   end
248 end
249
250 class Board
251 end
252
253 class Game
254   def initialize(game_name, player0, player1)
255     @game_name = game_name
256     if (player0.sente)
257       @sente = player0
258       @gote = player1
259     else
260       @sente = player0
261       @gote = player1
262     end
263     @current_player = @sente
264     @next_player = @gote
265
266     @sente.game = self
267     @gote.game = self
268     @sente.status = "agree_waiting"
269     @gote.status = "agree_waiting"
270     @id = sprintf("%s-%s-%s-%s", @game_name, @sente.name, @gote.name, Time::new.strftime("%Y%m%d%H%M%S"))
271     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
272
273     @logfile = @id + ".csa"
274     @board = Board::new
275     @start_time = nil
276     @fh = nil
277
278     propose
279   end
280   attr_accessor :game_name, :sente, :gote, :id, :board, :current_player, :next_player, :fh
281
282   def handle_one_move(str, player)
283     if (@current_player == player)
284       @end_time = Time::new
285       t = @end_time - @start_time
286       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
287       @sente.write_safe(sprintf("%s,T%d\n", str, t))
288       @gote.write_safe(sprintf("%s,T%d\n", str, t))
289       @current_player.mytime = @current_player.mytime - t
290       if (@current_player < 0)
291         timeout_end()
292       elsif (str =~ /%KACHI/)
293         kachi_end()
294       elsif (str =~ /%TORYO/)
295         toryo_end
296       end
297       (@current_player, @next_player) = [@next_player, @current_player]
298       @start_time = Time::new
299     end
300   end
301
302   def timeout_end
303     @current_player.status = "connected"
304     @next_player.status = "connected"
305     @current_player.write("#TIME_UP\n#LOSE\n")
306     @next_player.write("#TIME_UP\n#WIN\n")
307   end
308
309   def kachi_end
310     @current_player.status = "connected"
311     @next_player.status = "connected"
312     @current_player.write("#JISHOGI\n#WIN\n")
313     @next_player.write("#JISHOGI\n#LOSE\n")
314   end
315
316   def toryo_end
317     @current_player.status = "connected"
318     @next_player.status = "connected"
319     @current_player.write("#RESIGN\n#LOSE\n")
320     @next_player.write("#RESIGN\n#WIN\n")
321   end
322
323   def start
324     @sente.write_safe(sprintf("START:%s\n", @id))
325     @gote.write_safe(sprintf("START:%s\n", @id))
326     @start_time = Time::new
327   end
328
329   def propose
330     begin
331       @fh = open(@logfile, "w")
332       @fh.sync = true
333
334       @fh.printf("V2\n")
335       @fh.printf("N+%s\n", @sente.name)
336       @fh.printf("N-%s\n", @gote.name)
337       @fh.printf("$EVENT:%s\n", @id)
338
339       @sente.write_safe(propose_message("+"))
340       @gote.write_safe(propose_message("-"))
341
342       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
343       @fh.print <<EOM
344 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
345 P2 * -HI *  *  *  *  * -KA *
346 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
347 P4 *  *  *  *  *  *  *  *  *
348 P5 *  *  *  *  *  *  *  *  *
349 P6 *  *  *  *  *  *  *  *  *
350 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
351 P8 * +KA *  *  *  *  * +HI *
352 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
353 +
354 EOM
355     end
356   end
357
358   def propose_message(sg_flag)
359     str = <<EOM
360 Protocol_Mode:Server
361 Format:Shogi 1.0
362 Game_ID:#{@id}
363 Name+:#{@sente.name}
364 Name-:#{@gote.name}
365 Your_Turn:#{sg_flag}
366 Rematch_On_Draw:NO
367 To_Move:+
368 BEGIN Time
369 Time_Unit:1sec
370 Total_Time:#{Total_Time}
371 Least_Time_Per_Move:#{Least_Time_Per_Move}
372 END Time
373 BEGIN Position
374 Jishogi_Declaration:1.1
375 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
376 P2 * -HI *  *  *  *  * -KA *
377 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
378 P4 *  *  *  *  *  *  *  *  *
379 P5 *  *  *  *  *  *  *  *  *
380 P6 *  *  *  *  *  *  *  *  *
381 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
382 P8 * +KA *  *  *  *  * +HI *
383 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
384 P+
385 P-
386 +
387 END Position
388 END Game_Summary
389 EOM
390     return str
391   end
392 end
393
394 def usage
395     print <<EOM
396 NAME
397         shogi-server - server for CSA server protocol
398
399 SYNOPSIS
400         shogi-server event_name port_number
401
402 DESCRIPTION
403         server for CSA server protocol
404
405 OPTIONS
406         --pid-file file
407                 specify filename for logging process ID
408
409 LICENSE
410         this file is distributed under GPL version2 and might be compiled by Exerb
411
412 SEE ALSO
413
414 RELEASE
415         #{Release}
416
417 REVISION
418         #{Revision}
419 EOM
420 end
421
422 def log_message(str)
423   printf("%s message: %s\n", Time::new.to_s, str)
424 end
425
426 def log_warning(str)
427   printf("%s message: %s\n", Time::new.to_s, str)
428 end
429
430 def log_error(str)
431   printf("%s error: %s\n", Time::new.to_s, str)
432 end
433
434
435 def parse_command_line
436   options = Hash::new
437   parser = GetoptLong.new
438   parser.ordering = GetoptLong::REQUIRE_ORDER
439   parser.set_options(
440                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
441
442   begin
443     parser.each_option do |name, arg|
444       options[name] = arg.dup
445     end
446   rescue
447     usage
448     raise parser.error_message
449   end
450   return options
451 end
452
453 LEAGUE = League::new
454
455 def good_login?(str)
456   return false if (str !~ /^LOGIN /)
457   tokens = str.split
458   if ((tokens.length == 3) || (tokens.length == 4))
459     ## ok
460   else
461     return false
462   end
463   return true
464 end
465
466 def main
467   $options = parse_command_line
468   if (ARGV.length != 2)
469     usage
470     exit 2
471   end
472   event = ARGV.shift
473   port = ARGV.shift
474
475   Thread.abort_on_exception = true
476
477   server = TCPserver.open(port)
478   log_message("server started")
479
480   while true
481     Thread::start(server.accept) do |client|
482       client.sync = true
483       while (str = client.gets_timeout(Login_Time))
484         Thread::kill(Thread::current) if (! str) # disconnected
485         str =~ /([\r\n]*)$/
486         eol = $1
487         if (good_login?(str))
488           player = Player::new(str, client)
489           if (LEAGUE.duplicated?(player))
490             client.write_safe(sprintf("username %s is already connected%s", player.name, eol))
491             next
492           end
493           LEAGUE.add(player)
494           break
495         else
496           client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol)
497         end
498       end                       # login loop
499       log_message(sprintf("user %s login", player.name))
500       player.run
501       LEAGUE.delete(player)
502       log_message(sprintf("user %s logout", player.name))
503     end
504   end
505 end
506
507 if ($0 == __FILE__)
508   main
509 end