OSDN Git Service

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