#! /usr/bin/env ruby ## -*-Ruby-*- $RCSfile$ $Revision$ $Name$ ## Copyright (C) 2004 773@2ch ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA DEFAULT_TIMEOUT = 10 # for single socket operation Total_Time = 1500 Least_Time_Per_Move = 1 Watchdog_Time = 30 # time for ping Login_Time = 300 # time for LOGIN Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.') Release.concat("-") if (Release == "") Revision = "$Revision$".gsub(/[^\.\d]/, '') STDOUT.sync = true STDERR.sync = true require 'getoptlong' require 'thread' require 'timeout' require 'socket' require 'ping' TCPSocket.do_not_reverse_lookup = true class TCPSocket def gets_timeout(t = DEFAULT_TIMEOUT) begin timeout(t) do return self.gets end rescue TimeoutError return nil rescue return nil end end def gets_safe begin return self.gets rescue return nil end end def write_safe(str) begin return self.write(str) rescue return nil end end end class League def initialize @hash = Hash::new end attr_accessor :hash def add(player) @hash[player.name] = player end def delete(player) @hash.delete(player.name) end def duplicated?(player) if (@hash[player.name]) return true else return false end end def get_player(status, game_name, sente, searcher=nil) @hash.each do |name, player| if ((player.status == status) && (player.game_name == game_name) && ((player.sente == nil) || (player.sente == sente)) && ((searcher == nil) || (player != searcher))) return player end end return nil end def new_game(game_name, player0, player1) game = Game::new(game_name, player0, player1) end end class Player def initialize(str, socket) @name = nil @password = nil @socket = socket @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game @protocol = nil # CSA or x1 @eol = "\m" # favorite eol code @game = nil @game_name = "" @mytime = Total_Time # set in start method also @sente = nil @watchdog_thread = nil login(str) end attr_accessor :name, :password, :socket, :status attr_accessor :protocol, :eol, :game, :mytime, :watchdog_thread, :game_name, :sente def finish log_message(sprintf("user %s finish", @name)) Thread::kill(@watchdog_thread) if @watchdog_thread @socket.close if (! @socket.closed?) end def watchdog(time) while true begin Ping.pingecho(@socket.addr[3]) rescue end sleep(time) end end def to_s if ((status == "game_waiting") || (status == "agree_waiting") || (status == "game")) if (@sente) return sprintf("%s %s %s +", @name, @status, @game_name) elsif (@sente == false) return sprintf("%s %s %s -", @name, @status, @game_name) elsif (@sente == nil) return sprintf("%s %s %s +-", @name, @status, @game_name) end else return sprintf("%s %s", @name, @status) end end def write_help @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"') end def write_safe(str) @socket.write_safe(str.gsub(/[\r\n]+/, @eol)) end def login(str) str =~ /([\r\n]*)$/ @eol = $1 str.chomp! (login, @name, @password, ext) = str.split if (ext) @protocol = "x1" else @protocol = "CSA" end @watchdog_thread = Thread::start do watchdog(Watchdog_Time) end end def run write_safe(sprintf("LOGIN:%s OK\n", @name)) if (@protocol != "CSA") log_message(sprintf("user %s run in %s mode", @name, @protocol)) write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol)) else log_message(sprintf("user %s run in CSA mode", @name)) csa_1st_str = "%%GAME default +-" end while (csa_1st_str || (str = @socket.gets_safe)) begin $mutex.lock if (csa_1st_str) str = csa_1st_str csa_1st_str = nil end str.chomp! case str when /^[\+\-%][^%]/ if (@status == "game") s = @game.handle_one_move(str, self) return if (s && @protocol == "CSA") else next end when /^AGREE/ if (@status == "agree_waiting") @status = "start_waiting" if ((@game.sente.status == "start_waiting") && (@game.gote.status == "start_waiting")) @game.start @game.sente.status = "game" @game.gote.status = "game" end else write_safe("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status) next end when /^%%HELP/ write_help when /^%%GAME\s+(\S+)\s+([\+\-]+)/ if ((@status == "connected") || (@status == "game_waiting")) @status = "game_waiting" else write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status)) next end @status = "game_waiting" @game_name = $1 sente_str = $2 if (sente_str == "+") @sente = true rival_sente = false elsif (sente_str == "-") @sente = false rival_sente = true else @sente = nil rival_sente = nil end rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self) rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival) if (rival) if (@sente == nil) if (rand(2) == 0) @sente = true rival_sente = false else @sente = false rival_sente = true end elsif (rival_sente == nil) if (@sente) rival_sente = false else rival_sente = true end end rival.sente = rival_sente LEAGUE.new_game(@game_name, self, rival) self.status = "agree_waiting" rival.status = "agree_waiting" end when /^%%CHAT\s+(\S+)/ message = $1 LEAGUE.hash.each do |name, player| if (player.protocol != "CSA") s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) player.status = "zombie" if (! s) end end when /^%%WHO/ buf = Array::new LEAGUE.hash.each do |name, player| buf.push(sprintf("##[WHO] %s\n", player.to_s)) end buf.push("##[WHO] +OK\n") write_safe(buf.join) when /^LOGOUT/ write_safe("LOGOUT:completed\n") finish return else write_safe(sprintf("##[ERROR] unknown command %s\n", str)) end ensure $mutex.unlock end end # enf of while end end class Board end class Game def initialize(game_name, player0, player1) @game_name = game_name if (player0.sente) @sente = player0 @gote = player1 else @sente = player1 @gote = player0 end @current_player = @sente @next_player = @gote @sente.game = self @gote.game = self @sente.status = "agree_waiting" @gote.status = "agree_waiting" @id = sprintf("%s-%s-%s-%s", @game_name, @sente.name, @gote.name, Time::new.strftime("%Y%m%d%H%M%S")) log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name)) @logfile = @id + ".csa" @board = Board::new @start_time = nil @fh = nil propose end attr_accessor :game_name, :sente, :gote, :id, :board, :current_player, :next_player, :fh def finish log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name)) @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S")) @fh.close @sente.status = "connected" @gote.status = "connected" if (@current_player.protocol == "CSA") @current_player.finish end end def handle_one_move(str, player) finish_flag = false if (@current_player == player) @end_time = Time::new t = @end_time - @start_time t = Least_Time_Per_Move if (t < Least_Time_Per_Move) @sente.write_safe(sprintf("%s,T%d\n", str, t)) @gote.write_safe(sprintf("%s,T%d\n", str, t)) @fh.printf("%s\nT%d\n", str, t) @current_player.mytime = @current_player.mytime - t if (@current_player.mytime < 0) timeout_end() finish_flag = true elsif (str =~ /%KACHI/) kachi_end() finish_flag = true elsif (str =~ /%TORYO/) toryo_end finish_flag = true end (@current_player, @next_player) = [@next_player, @current_player] @start_time = Time::new finish if (finish_flag) return finish_flag end end def timeout_end @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#TIME_UP\n#LOSE\n") @next_player.write_safe("#TIME_UP\n#WIN\n") end def kachi_end @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#JISHOGI\n#WIN\n") @next_player.write_safe("#JISHOGI\n#LOSE\n") end def toryo_end @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#RESIGN\n#LOSE\n") @next_player.write_safe("#RESIGN\n#WIN\n") end def start log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name)) @sente.write_safe(sprintf("START:%s\n", @id)) @gote.write_safe(sprintf("START:%s\n", @id)) @mytime = Total_Time @start_time = Time::new end def propose begin @fh = open(@logfile, "w") @fh.sync = true @fh.printf("V2\n") @fh.printf("N+%s\n", @sente.name) @fh.printf("N-%s\n", @gote.name) @fh.printf("$EVENT:%s\n", @id) @sente.write_safe(propose_message("+")) @gote.write_safe(propose_message("-")) @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S")) @fh.print <