OSDN Git Service

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