OSDN Git Service

%%LOGOUT -> LOGOUT
[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, searcher=nil)
90     @hash.each do |name, player|
91       if ((player.status == status) &&
92           (player.game_name == game_name) &&
93           ((player.sente == nil) || (player.sente == sente)) &&
94           ((searcher == nil) || (player != searcher)))
95         return player
96       end
97     end
98     return nil
99   end
100   def new_game(game_name, player0, player1)
101     game = Game::new(game_name, player0, player1)
102   end
103 end
104
105
106
107
108 class Player
109   def initialize(str, socket)
110     @name = nil
111     @password = nil
112     @socket = socket
113     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game
114
115     @protocol = nil             # CSA or x1
116     @eol = "\m"                 # favorite eol code
117     @game = nil
118     @game_name = ""
119     @mytime = Total_Time
120     @sente = nil
121     @watchdog_thread = nil
122
123     login(str)
124   end
125
126   attr_accessor :name, :password, :socket, :status
127   attr_accessor :protocol, :eol, :game, :mytime, :watchdog_thread, :game_name, :sente
128
129   def finish
130     log_message(sprintf("user %s finish", @name))    
131     Thread::kill(@watchdog_thread) if @watchdog_thread
132     @socket.close if (! @socket.closed?)
133   end
134
135   def watchdog(time)
136     while true
137       begin
138         Ping.pingecho(@socket.addr[3])
139       rescue
140       end
141       sleep(time)
142     end
143   end
144
145   def to_s
146     if ((status == "game_waiting") ||
147         (status == "agree_waiting") ||
148         (status == "game"))
149       if (@sente)
150         return sprintf("%s %s %s +", @name, @status, @game_name)
151       elsif (@sente == false)
152         return sprintf("%s %s %s -", @name, @status, @game_name)
153       elsif (@sente == nil)
154         return sprintf("%s %s %s +-", @name, @status, @game_name)
155       end
156     else
157       return sprintf("%s %s", @name, @status)
158     end
159   end
160
161   def write_help
162     @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
163   end
164
165   def write_safe(str)
166     @socket.write_safe(str.gsub(/[\r\n]+/, @eol))
167   end
168
169   def login(str)
170     str =~ /([\r\n]*)$/
171     @eol = $1
172     str.chomp!
173     (login, @name, @password, ext) = str.split
174     if (ext)
175       @protocol = "x1"
176     else
177       @protocol = "CSA"
178     end
179     @watchdog_thread = Thread::start do
180       watchdog(Watchdog_Time)
181     end
182   end
183     
184   def run
185     write_safe(sprintf("LOGIN:%s OK\n", @name))
186     if (@protocol != "CSA")
187       log_message(sprintf("user %s run in %s mode", @name, @protocol))
188       write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
189     else
190       log_message(sprintf("user %s run in CSA mode", @name))
191       csa_1st_str = "%%GAME default +-"
192     end
193
194     
195     while (csa_1st_str || (str = @socket.gets_safe))
196       begin
197         $mutex.lock
198         if (csa_1st_str)
199           str = csa_1st_str
200           csa_1st_str = nil
201         end
202         str.chomp!
203         case str
204         when /^[\+\-%][^%]/
205           if (@status == "game")
206             s = @game.handle_one_move(str, self)
207             return if (s && @protocol == "CSA")
208           else
209             next
210           end
211         when /^AGREE/
212           if (@status == "agree_waiting")
213             @status = "start_waiting"
214             if ((@game.sente.status == "start_waiting") &&
215                 (@game.gote.status == "start_waiting"))
216               @game.start
217               @game.sente.status = "game"
218               @game.gote.status = "game"
219             end
220           else
221             write_safe("## you are in %s status. AGREE is valid in agree_waiting status\n", @status)
222             next
223           end
224         when /^%%HELP/
225           write_help
226         when /^%%GAME\s+(\S+)\s+([\+\-]+)/
227           if ((@status == "connected") || (@status == "game_waiting"))
228             @status = "game_waiting"
229           else
230             write_safe(sprintf("## you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
231             next
232           end
233           @status = "game_waiting"
234           @game_name = $1
235           sente_str = $2
236           if (sente_str == "+")
237             @sente = true
238             rival_sente = false
239           elsif (sente_str == "-")
240             @sente = false
241             rival_sente = true
242           else
243             @sente = nil
244             rival_sente = nil
245           end
246           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
247           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
248           if (rival)
249             if (@sente == nil)
250               if (rand(2) == 0)
251                 @sente = true
252                 rival_sente = false
253               else
254                 @sente = false
255                 rival_sente = true
256               end
257             elsif (rival_sente == nil)
258               if (@sente)
259                 rival_sente = false
260               else
261                 rival_sente = true
262               end
263             end
264             rival.sente = rival_sente
265             LEAGUE.new_game(@game_name, self, rival)
266             self.status = "agree_waiting"
267             rival.status = "agree_waiting"
268           end
269         when /^%%CHAT\s+(\S+)/
270           message = $1
271           LEAGUE.hash.each do |name, player|
272             if (player.protocol != "CSA")
273               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
274               player.status = "zombie" if (! s)
275             end
276           end
277         when /^%%WHO/
278           buf = Array::new
279           LEAGUE.hash.each do |name, player|
280             buf.push(sprintf("##[WHO] %s\n", player.to_s))
281           end
282           buf.push("##[WHO] +OK\n")
283           write_safe(buf.join)
284         when /^LOGOUT/
285           write_safe("LOGOUT:completed\n")
286           finish
287           return
288         else
289           write_safe(sprintf("## unknown command %s\n", str))
290         end
291       ensure
292         $mutex.unlock
293       end
294     end                         # enf of while
295   end
296 end
297
298 class Board
299 end
300
301 class Game
302   def initialize(game_name, player0, player1)
303     @game_name = game_name
304     if (player0.sente)
305       @sente = player0
306       @gote = player1
307     else
308       @sente = player1
309       @gote = player0
310     end
311     @current_player = @sente
312     @next_player = @gote
313
314     @sente.game = self
315     @gote.game = self
316     @sente.status = "agree_waiting"
317     @gote.status = "agree_waiting"
318     @id = sprintf("%s-%s-%s-%s", @game_name, @sente.name, @gote.name, Time::new.strftime("%Y%m%d%H%M%S"))
319     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
320
321     @logfile = @id + ".csa"
322     @board = Board::new
323     @start_time = nil
324     @fh = nil
325
326     propose
327   end
328   attr_accessor :game_name, :sente, :gote, :id, :board, :current_player, :next_player, :fh
329
330   def finish
331     log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name))
332     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
333     @fh.close
334     @sente.status = "connected"
335     @gote.status = "connected"
336     if (@current_player.protocol == "CSA")
337       @current_player.finish
338     end
339   end
340
341   def handle_one_move(str, player)
342     finish_flag = false
343     if (@current_player == player)
344       @end_time = Time::new
345       t = @end_time - @start_time
346       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
347       @sente.write_safe(sprintf("%s,T%d\n", str, t))
348       @gote.write_safe(sprintf("%s,T%d\n", str, t))
349       @fh.printf("%s\nT%d\n", str, t)
350       @current_player.mytime = @current_player.mytime - t
351       if (@current_player.mytime < 0)
352         timeout_end()
353         finish_flag = true
354       elsif (str =~ /%KACHI/)
355         kachi_end()
356         finish_flag = true
357       elsif (str =~ /%TORYO/)
358         toryo_end
359         finish_flag = true
360       end
361       (@current_player, @next_player) = [@next_player, @current_player]
362       @start_time = Time::new
363       finish if (finish_flag)
364       return finish_flag
365     end
366   end
367
368   def timeout_end
369     @current_player.status = "connected"
370     @next_player.status = "connected"
371     @current_player.write_safe("#TIME_UP\n#LOSE\n")
372     @next_player.write_safe("#TIME_UP\n#WIN\n")
373   end
374
375   def kachi_end
376     @current_player.status = "connected"
377     @next_player.status = "connected"
378     @current_player.write_safe("#JISHOGI\n#WIN\n")
379     @next_player.write_safe("#JISHOGI\n#LOSE\n")
380   end
381
382   def toryo_end
383     @current_player.status = "connected"
384     @next_player.status = "connected"
385     @current_player.write_safe("#RESIGN\n#LOSE\n")
386     @next_player.write_safe("#RESIGN\n#WIN\n")
387   end
388
389   def start
390     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
391     @sente.write_safe(sprintf("START:%s\n", @id))
392     @gote.write_safe(sprintf("START:%s\n", @id))
393     @start_time = Time::new
394   end
395
396   def propose
397     begin
398       @fh = open(@logfile, "w")
399       @fh.sync = true
400
401       @fh.printf("V2\n")
402       @fh.printf("N+%s\n", @sente.name)
403       @fh.printf("N-%s\n", @gote.name)
404       @fh.printf("$EVENT:%s\n", @id)
405
406       @sente.write_safe(propose_message("+"))
407       @gote.write_safe(propose_message("-"))
408
409       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
410       @fh.print <<EOM
411 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
412 P2 * -HI *  *  *  *  * -KA *
413 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
414 P4 *  *  *  *  *  *  *  *  *
415 P5 *  *  *  *  *  *  *  *  *
416 P6 *  *  *  *  *  *  *  *  *
417 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
418 P8 * +KA *  *  *  *  * +HI *
419 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
420 +
421 EOM
422     end
423   end
424
425   def propose_message(sg_flag)
426     str = <<EOM
427 Protocol_Mode:Server
428 Format:Shogi 1.0
429 Game_ID:#{@id}
430 Name+:#{@sente.name}
431 Name-:#{@gote.name}
432 Your_Turn:#{sg_flag}
433 Rematch_On_Draw:NO
434 To_Move:+
435 BEGIN Time
436 Time_Unit:1sec
437 Total_Time:#{Total_Time}
438 Least_Time_Per_Move:#{Least_Time_Per_Move}
439 END Time
440 BEGIN Position
441 Jishogi_Declaration:1.1
442 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
443 P2 * -HI *  *  *  *  * -KA *
444 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
445 P4 *  *  *  *  *  *  *  *  *
446 P5 *  *  *  *  *  *  *  *  *
447 P6 *  *  *  *  *  *  *  *  *
448 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
449 P8 * +KA *  *  *  *  * +HI *
450 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
451 P+
452 P-
453 +
454 END Position
455 END Game_Summary
456 EOM
457     return str
458   end
459 end
460
461 def usage
462     print <<EOM
463 NAME
464         shogi-server - server for CSA server protocol
465
466 SYNOPSIS
467         shogi-server event_name port_number
468
469 DESCRIPTION
470         server for CSA server protocol
471
472 OPTIONS
473         --pid-file file
474                 specify filename for logging process ID
475
476 LICENSE
477         this file is distributed under GPL version2 and might be compiled by Exerb
478
479 SEE ALSO
480
481 RELEASE
482         #{Release}
483
484 REVISION
485         #{Revision}
486 EOM
487 end
488
489 def log_message(str)
490   printf("%s message: %s\n", Time::new.to_s, str)
491 end
492
493 def log_warning(str)
494   printf("%s message: %s\n", Time::new.to_s, str)
495 end
496
497 def log_error(str)
498   printf("%s error: %s\n", Time::new.to_s, str)
499 end
500
501
502 def parse_command_line
503   options = Hash::new
504   parser = GetoptLong.new
505   parser.ordering = GetoptLong::REQUIRE_ORDER
506   parser.set_options(
507                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
508
509   parser.quiet = true
510   begin
511     parser.each_option do |name, arg|
512       name.sub!(/^--/, '')
513       options[name] = arg.dup
514     end
515   rescue
516     usage
517     raise parser.error_message
518   end
519   return options
520 end
521
522 LEAGUE = League::new
523
524 def good_login?(str)
525   return false if (str !~ /^LOGIN /)
526   tokens = str.split
527   if ((tokens.length == 3) || 
528       ((tokens.length == 4) && tokens[3] == "x1"))
529     ## ok
530   else
531     return false
532   end
533   return true
534 end
535
536 def  write_pid_file(file)
537   open(file, "w") do |fh|
538     fh.print Process::pid, "\n"
539   end
540 end
541
542 def main
543   $mutex = Mutex::new
544   $options = parse_command_line
545   if (ARGV.length != 2)
546     usage
547     exit 2
548   end
549   event = ARGV.shift
550   port = ARGV.shift
551
552   write_pid_file($options["pid-file"]) if ($options["pid-file"])
553
554
555   Thread.abort_on_exception = true
556
557   server = TCPserver.open(port)
558   log_message("server started")
559
560   while true
561     Thread::start(server.accept) do |client|
562       client.sync = true
563       player = nil
564       while (str = client.gets_timeout(Login_Time))
565         begin
566           $mutex.lock
567           Thread::kill(Thread::current) if (! str) # disconnected
568           str =~ /([\r\n]*)$/
569           eol = $1
570           if (good_login?(str))
571             player = Player::new(str, client)
572             if (LEAGUE.duplicated?(player))
573               client.write_safe("LOGIN:incorrect" + eol)
574               client.write_safe(sprintf("username %s is already connected%s", player.name, eol))
575               client.close
576               Thread::kill(Thread::current)
577             end
578             LEAGUE.add(player)
579             break
580           else
581             client.write_safe("LOGIN:incorrect" + eol)
582             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol)
583             client.close
584             Thread::kill(Thread::current)
585           end
586         ensure
587           $mutex.unlock
588         end
589       end                       # login loop
590       log_message(sprintf("user %s login", player.name))
591       player.run
592       LEAGUE.delete(player)
593       log_message(sprintf("user %s logout", player.name))
594     end
595   end
596 end
597
598 if ($0 == __FILE__)
599   main
600 end