OSDN Git Service

127efaf70672600b249ea9a9991adc0c363387e1
[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     @games = Hash::new
85     @players = Hash::new
86   end
87   attr_accessor :players, :games
88
89   def add(player)
90     @players[player.name] = player
91   end
92   def delete(player)
93     @players.delete(player.name)
94   end
95   def duplicated?(player)
96     if (@players[player.name])
97       return true
98     else
99       return false
100     end
101   end
102   def get_player(status, game_name, sente, searcher=nil)
103     @players.each do |name, player|
104       if ((player.status == status) &&
105           (player.game_name == game_name) &&
106           ((player.sente == nil) || (player.sente == sente)) &&
107           ((searcher == nil) || (player != searcher)))
108         return player
109       end
110     end
111     return nil
112   end
113 end
114
115 class Player
116   def initialize(str, socket)
117     @name = nil
118     @password = nil
119     @socket = socket
120     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game
121
122     @protocol = nil             # CSA or x1
123     @eol = "\m"                 # favorite eol code
124     @game = nil
125     @game_name = ""
126     @mytime = Total_Time        # set in start method also
127     @sente = nil
128     @watchdog_thread = nil
129
130     login(str)
131   end
132
133   attr_accessor :name, :password, :socket, :status
134   attr_accessor :protocol, :eol, :game, :mytime, :watchdog_thread, :game_name, :sente
135
136   def finish
137     log_message(sprintf("user %s finish", @name))    
138     Thread::kill(@watchdog_thread) if @watchdog_thread
139     @socket.close if (! @socket.closed?)
140   end
141
142   def watchdog(time)
143     while true
144       begin
145         Ping.pingecho(@socket.addr[3])
146       rescue
147       end
148       sleep(time)
149     end
150   end
151
152   def to_s
153     if ((status == "game_waiting") ||
154         (status == "agree_waiting") ||
155         (status == "game"))
156       if (@sente)
157         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
158       elsif (@sente == false)
159         return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
160       elsif (@sente == nil)
161         return sprintf("%s %s %s %s +-", @name, @protocol, @status, @game_name)
162       end
163     else
164       return sprintf("%s %s %s", @name, @protocol, @status)
165     end
166   end
167
168   def write_help
169     @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
170   end
171
172   def write_safe(str)
173     @socket.write_safe(str.gsub(/[\r\n]+/, @eol))
174   end
175
176   def login(str)
177     str =~ /([\r\n]*)$/
178     @eol = $1
179     str.chomp!
180     (login, @name, @password, ext) = str.split
181     if (ext)
182       @protocol = "x1"
183     else
184       @protocol = "CSA"
185     end
186     @watchdog_thread = Thread::start do
187       watchdog(Watchdog_Time)
188     end
189   end
190     
191   def run
192     write_safe(sprintf("LOGIN:%s OK\n", @name))
193     if (@protocol != "CSA")
194       log_message(sprintf("user %s run in %s mode", @name, @protocol))
195       write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
196     else
197       log_message(sprintf("user %s run in CSA mode", @name))
198       csa_1st_str = "%%GAME default +-"
199     end
200
201     
202     while (csa_1st_str || (str = @socket.gets_safe(@mytime)))
203       begin
204         $mutex.lock
205         if (csa_1st_str)
206           str = csa_1st_str
207           csa_1st_str = nil
208         end
209         str.chomp! if (str.class == String)
210         case str
211         when /^[\+\-%][^%]/, :timeout
212           if (@status == "game")
213             s = @game.handle_one_move(str, self)
214             return if (s && @protocol == "CSA")
215           else
216             next
217           end
218         when /^AGREE/
219           if (@status == "agree_waiting")
220             @status = "start_waiting"
221             if ((@game.sente.status == "start_waiting") &&
222                 (@game.gote.status == "start_waiting"))
223               @game.start
224               @game.sente.status = "game"
225               @game.gote.status = "game"
226             end
227           else
228             write_safe("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)
229             next
230           end
231         when /^%%HELP/
232           write_help
233         when /^%%GAME\s+(\S+)\s+([\+\-]+)/
234           if ((@status == "connected") || (@status == "game_waiting"))
235             @status = "game_waiting"
236           else
237             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
238             next
239           end
240           @status = "game_waiting"
241           @game_name = $1
242           sente_str = $2
243           if (sente_str == "+")
244             @sente = true
245             rival_sente = false
246           elsif (sente_str == "-")
247             @sente = false
248             rival_sente = true
249           else
250             @sente = nil
251             rival_sente = nil
252           end
253           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
254           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
255           if (rival)
256             if (@sente == nil)
257               if (rand(2) == 0)
258                 @sente = true
259                 rival_sente = false
260               else
261                 @sente = false
262                 rival_sente = true
263               end
264             elsif (rival_sente == nil)
265               if (@sente)
266                 rival_sente = false
267               else
268                 rival_sente = true
269               end
270             end
271             rival.sente = rival_sente
272             Game::new(@game_name, self, rival)
273             self.status = "agree_waiting"
274             rival.status = "agree_waiting"
275           end
276         when /^%%CHAT\s+(.+)/
277           message = $1
278           LEAGUE.players.each do |name, player|
279             if (player.protocol != "CSA")
280               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
281               player.status = "zombie" if (! s)
282             end
283           end
284         when /^%%LIST/
285           buf = Array::new
286           LEAGUE.games.each do |id, game|
287             buf.push(sprintf("##[LIST] %s\n", id))
288           end
289           buf.push("##[LIST] +OK\n")
290           write_safe(buf.join)
291         when /^%%SHOW\s+(\S+)/
292           id = $1
293           if (LEAGUE.games[id])
294             write_safe(LEAGUE.games[id].board.to_s.gsub(/^/, '##[SHOW] '))
295           end
296           write_safe("##[SHOW] +OK\n")
297         when /^%%WHO/
298           buf = Array::new
299           LEAGUE.players.each do |name, player|
300             buf.push(sprintf("##[WHO] %s\n", player.to_s))
301           end
302           buf.push("##[WHO] +OK\n")
303           write_safe(buf.join)
304         when /^LOGOUT/
305           write_safe("LOGOUT:completed\n")
306           finish
307           return
308         else
309           write_safe(sprintf("##[ERROR] unknown command %s\n", str))
310         end
311       ensure
312         $mutex.unlock
313       end
314     end                         # enf of while
315   end
316 end
317
318 class Piece
319   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
320   def initialize(name, sente)
321     @name = name
322     @sente = sente
323     @promoted = false
324   end
325   attr_accessor :name, :promoted, :sente
326
327   def promoted_name
328     PROMOTE[name]
329   end
330
331   def to_s
332     if (@sente)
333       sg = "+"
334     else
335       sg = "-"
336     end
337     if (@promoted)
338       n = PROMOTE[@name]
339     else
340       n = @name
341     end
342     return sg + n
343   end
344 end
345
346
347
348 class Board
349   def initialize
350     @sente_hands = Array::new
351     @gote_hands = Array::new
352     @array = [[], [], [], [], [], [], [], [], [], []]
353   end
354   attr_accessor :array, :sente_hands, :gote_hands
355
356   def initial
357     @array[1][1] = Piece::new("KY", false)
358     @array[2][1] = Piece::new("KE", false)
359     @array[3][1] = Piece::new("GI", false)
360     @array[4][1] = Piece::new("KI", false)
361     @array[5][1] = Piece::new("OU", false)
362     @array[6][1] = Piece::new("KI", false)
363     @array[7][1] = Piece::new("GI", false)
364     @array[8][1] = Piece::new("KE", false)
365     @array[9][1] = Piece::new("KY", false)
366     @array[2][2] = Piece::new("KA", false)
367     @array[8][2] = Piece::new("HI", false)
368     @array[1][3] = Piece::new("FU", false)
369     @array[2][3] = Piece::new("FU", false)
370     @array[3][3] = Piece::new("FU", false)
371     @array[4][3] = Piece::new("FU", false)
372     @array[5][3] = Piece::new("FU", false)
373     @array[6][3] = Piece::new("FU", false)
374     @array[7][3] = Piece::new("FU", false)
375     @array[8][3] = Piece::new("FU", false)
376     @array[9][3] = Piece::new("FU", false)
377
378     @array[1][9] = Piece::new("KY", true)
379     @array[2][9] = Piece::new("KE", true)
380     @array[3][9] = Piece::new("GI", true)
381     @array[4][9] = Piece::new("KI", true)
382     @array[5][9] = Piece::new("OU", true)
383     @array[6][9] = Piece::new("KI", true)
384     @array[7][9] = Piece::new("GI", true)
385     @array[8][9] = Piece::new("KE", true)
386     @array[9][9] = Piece::new("KY", true)
387     @array[2][8] = Piece::new("HI", true)
388     @array[8][8] = Piece::new("KA", true)
389     @array[1][7] = Piece::new("FU", true)
390     @array[2][7] = Piece::new("FU", true)
391     @array[3][7] = Piece::new("FU", true)
392     @array[4][7] = Piece::new("FU", true)
393     @array[5][7] = Piece::new("FU", true)
394     @array[6][7] = Piece::new("FU", true)
395     @array[7][7] = Piece::new("FU", true)
396     @array[8][7] = Piece::new("FU", true)
397     @array[9][7] = Piece::new("FU", true)
398   end
399
400   def get_piece_from_hands(hands, name)
401     p = hands.find { |i|
402       i.name == name
403     }
404     if (p)
405       hands.delete(p)
406     end
407     return p
408   end
409
410   def handle_one_move(str)
411     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
412       p = $1
413       x0 = $2.to_i
414       y0 = $3.to_i
415       x1 = $4.to_i
416       y1 = $5.to_i
417       name = $6
418     elsif (str =~ /^%/)
419       return true
420     else
421       return false              # illegal move
422     end
423     if (p == "+")
424       sente = true
425       hands = @sente_hands
426     else
427       sente = false
428       hands = @gote_hands
429     end
430     if (@array[x1][y1])
431       if (@array[x1][y1] == sente) # this is mine
432         return false            
433       end
434       hands.push(@array[x1][y1])
435       @array[x1][y1] = nil
436     end
437     if ((x0 == 0) && (y0 == 0))
438       p = get_piece_from_hands(hands, name)
439       return false if (! p)     # i don't have this one
440       @array[x1][y1] = p
441       p.sente = sente
442       p.promoted = false
443     else
444       @array[x1][y1] = @array[x0][y0]
445       @array[x0][y0] = nil
446       if (@array[x1][y1].name != name) # promoted ?
447         return false if (@array[x1][y1].promoted_name != name) # can't promote
448         @array[x1][y1].promoted = true
449       end
450     end
451     return true                 # legal move
452   end
453
454   def to_s
455     a = Array::new
456     y = 1
457     while (y <= 9)
458       a.push(sprintf("P%d", y))
459       x = 9
460       while (x >= 1)
461         piece = @array[x][y]
462         if (piece)
463           s = piece.to_s
464         else
465           s = " * "
466         end
467         a.push(s)
468         x = x - 1
469       end
470       a.push(sprintf("\n"))
471       y = y + 1
472     end
473     if (! sente_hands.empty?)
474       a.push("P+")
475       sente_hands.each do |p|
476         a.push("00" + p.name)
477       end
478       a.push("\n")
479     end
480     if (! gote_hands.empty?)
481       a.push("P-")
482       gote_hands.each do |p|
483         a.push("00" + p.name)
484       end
485       a.push("\n")
486     end
487     return a.join
488   end
489 end
490
491 class Game
492   def initialize(game_name, player0, player1)
493     @game_name = game_name
494     if (player0.sente)
495       @sente = player0
496       @gote = player1
497     else
498       @sente = player1
499       @gote = player0
500     end
501     @current_player = @sente
502     @next_player = @gote
503
504     @sente.game = self
505     @gote.game = self
506
507     @sente.status = "agree_waiting"
508     @gote.status = "agree_waiting"
509     @id = sprintf("%s-%s-%s-%s", @game_name, @sente.name, @gote.name, Time::new.strftime("%Y%m%d%H%M%S"))
510     LEAGUE.games[@id] = self
511
512
513     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
514
515     @logfile = @id + ".csa"
516     @board = Board::new
517     @board.initial
518     @start_time = nil
519     @fh = nil
520
521     propose
522   end
523   attr_accessor :game_name, :sente, :gote, :id, :board, :current_player, :next_player, :fh
524
525   def finish
526     log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name))
527     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
528     @fh.close
529     @sente.status = "connected"
530     @gote.status = "connected"
531     if (@current_player.protocol == "CSA")
532       @current_player.finish
533     end
534     LEAGUE.games.delete(@id)
535   end
536
537   def handle_one_move(str, player)
538     finish_flag = false
539     if (@current_player == player)
540       @end_time = Time::new
541       t = @end_time - @start_time
542       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
543       if (str != :timeout)
544         legal_move = @board.handle_one_move(str)
545         if (legal_move)
546           @sente.write_safe(sprintf("%s,T%d\n", str, t))
547           @gote.write_safe(sprintf("%s,T%d\n", str, t))
548           @fh.printf("%s\nT%d\n", str, t)
549         else
550           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
551         end
552         @current_player.mytime = @current_player.mytime - t
553       else
554         @current_player.mytime = 0
555       end
556       if (!legal_move)
557         illegal_end()
558         finish_flag = true
559       elsif (@current_player.mytime <= 0)
560         timeout_end()
561         finish_flag = true
562       elsif (str =~ /%KACHI/)
563         kachi_end()
564         finish_flag = true
565       elsif (str =~ /%TORYO/)
566         toryo_end()
567         finish_flag = true
568       end
569       (@current_player, @next_player) = [@next_player, @current_player]
570       @start_time = Time::new
571       finish if (finish_flag)
572       return finish_flag
573     end
574   end
575
576   def illegal_end
577     @current_player.status = "connected"
578     @next_player.status = "connected"
579     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
580     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
581   end
582
583   def timeout_end
584     @current_player.status = "connected"
585     @next_player.status = "connected"
586     @current_player.write_safe("#TIME_UP\n#LOSE\n")
587     @next_player.write_safe("#TIME_UP\n#WIN\n")
588   end
589
590   def kachi_end
591     @current_player.status = "connected"
592     @next_player.status = "connected"
593     @current_player.write_safe("#JISHOGI\n#WIN\n")
594     @next_player.write_safe("#JISHOGI\n#LOSE\n")
595   end
596
597   def toryo_end
598     @current_player.status = "connected"
599     @next_player.status = "connected"
600     @current_player.write_safe("#RESIGN\n#LOSE\n")
601     @next_player.write_safe("#RESIGN\n#WIN\n")
602   end
603
604   def start
605     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
606     @sente.write_safe(sprintf("START:%s\n", @id))
607     @gote.write_safe(sprintf("START:%s\n", @id))
608     @mytime = Total_Time    
609     @start_time = Time::new
610   end
611
612   def propose
613     begin
614       @fh = open(@logfile, "w")
615       @fh.sync = true
616
617       @fh.printf("V2\n")
618       @fh.printf("N+%s\n", @sente.name)
619       @fh.printf("N-%s\n", @gote.name)
620       @fh.printf("$EVENT:%s\n", @id)
621
622       @sente.write_safe(propose_message("+"))
623       @gote.write_safe(propose_message("-"))
624
625       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
626       @fh.print <<EOM
627 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
628 P2 * -HI *  *  *  *  * -KA *
629 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
630 P4 *  *  *  *  *  *  *  *  *
631 P5 *  *  *  *  *  *  *  *  *
632 P6 *  *  *  *  *  *  *  *  *
633 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
634 P8 * +KA *  *  *  *  * +HI *
635 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
636 +
637 EOM
638     end
639   end
640
641   def propose_message(sg_flag)
642     str = <<EOM
643 Protocol_Mode:Server
644 Format:Shogi 1.0
645 Game_ID:#{@id}
646 Name+:#{@sente.name}
647 Name-:#{@gote.name}
648 Your_Turn:#{sg_flag}
649 Rematch_On_Draw:NO
650 To_Move:+
651 BEGIN Time
652 Time_Unit:1sec
653 Total_Time:#{Total_Time}
654 Least_Time_Per_Move:#{Least_Time_Per_Move}
655 END Time
656 BEGIN Position
657 Jishogi_Declaration:1.1
658 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
659 P2 * -HI *  *  *  *  * -KA *
660 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
661 P4 *  *  *  *  *  *  *  *  *
662 P5 *  *  *  *  *  *  *  *  *
663 P6 *  *  *  *  *  *  *  *  *
664 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
665 P8 * +KA *  *  *  *  * +HI *
666 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
667 P+
668 P-
669 +
670 END Position
671 END Game_Summary
672 EOM
673     return str
674   end
675 end
676
677 def usage
678     print <<EOM
679 NAME
680         shogi-server - server for CSA server protocol
681
682 SYNOPSIS
683         shogi-server event_name port_number
684
685 DESCRIPTION
686         server for CSA server protocol
687
688 OPTIONS
689         --pid-file file
690                 specify filename for logging process ID
691
692 LICENSE
693         this file is distributed under GPL version2 and might be compiled by Exerb
694
695 SEE ALSO
696
697 RELEASE
698         #{Release}
699
700 REVISION
701         #{Revision}
702 EOM
703 end
704
705 def log_message(str)
706   printf("%s message: %s\n", Time::new.to_s, str)
707 end
708
709 def log_warning(str)
710   printf("%s message: %s\n", Time::new.to_s, str)
711 end
712
713 def log_error(str)
714   printf("%s error: %s\n", Time::new.to_s, str)
715 end
716
717
718 def parse_command_line
719   options = Hash::new
720   parser = GetoptLong.new
721   parser.ordering = GetoptLong::REQUIRE_ORDER
722   parser.set_options(
723                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
724
725   parser.quiet = true
726   begin
727     parser.each_option do |name, arg|
728       name.sub!(/^--/, '')
729       options[name] = arg.dup
730     end
731   rescue
732     usage
733     raise parser.error_message
734   end
735   return options
736 end
737
738 LEAGUE = League::new
739
740 def good_login?(str)
741   return false if (str !~ /^LOGIN /)
742   tokens = str.split
743   if ((tokens.length == 3) || 
744       ((tokens.length == 4) && tokens[3] == "x1"))
745     ## ok
746   else
747     return false
748   end
749   return true
750 end
751
752 def  write_pid_file(file)
753   open(file, "w") do |fh|
754     fh.print Process::pid, "\n"
755   end
756 end
757
758 def main
759   $mutex = Mutex::new
760   $options = parse_command_line
761   if (ARGV.length != 2)
762     usage
763     exit 2
764   end
765   event = ARGV.shift
766   port = ARGV.shift
767
768   write_pid_file($options["pid-file"]) if ($options["pid-file"])
769
770
771   Thread.abort_on_exception = true
772
773   server = TCPserver.open(port)
774   log_message("server started")
775
776   while true
777     Thread::start(server.accept) do |client|
778       client.sync = true
779       player = nil
780       while (str = client.gets_timeout(Login_Time))
781         begin
782           $mutex.lock
783           Thread::kill(Thread::current) if (! str) # disconnected
784           str =~ /([\r\n]*)$/
785           eol = $1
786           if (good_login?(str))
787             player = Player::new(str, client)
788             if (LEAGUE.duplicated?(player))
789               client.write_safe("LOGIN:incorrect" + eol)
790               client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
791               client.close
792               Thread::kill(Thread::current)
793             end
794             LEAGUE.add(player)
795             break
796           else
797             client.write_safe("LOGIN:incorrect" + eol)
798             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
799             client.close
800             Thread::kill(Thread::current)
801           end
802         ensure
803           $mutex.unlock
804         end
805       end                       # login loop
806       log_message(sprintf("user %s login", player.name))
807       player.run
808       LEAGUE.delete(player)
809       log_message(sprintf("user %s logout", player.name))
810     end
811   end
812 end
813
814 if ($0 == __FILE__)
815   main
816 end