shogi-server source
Revisión | 9c124efa7d68cb35fc3e837f2abae76ead13c9ed (tree) |
---|---|
Tiempo | 2004-05-30 13:26:52 |
Autor | nabeken <nabeken@b8c6...> |
Commiter | nabeken |
initial
@@ -0,0 +1,447 @@ | ||
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 |