#! /usr/bin/env ruby ## -*-Ruby-*- $RCSfile$ $Revision$ $Name$ ## Copyright (C) 2004 nanami@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(t = nil) if (t && t > 0) begin timeout(t) do return self.gets end rescue TimeoutError return :timeout rescue return nil end else begin return self.gets rescue return nil end end end def write_safe(str) begin return self.write(str) rescue return nil end end end class League def initialize @games = Hash::new @players = Hash::new end attr_accessor :players, :games def add(player) @players[player.name] = player end def delete(player) @players.delete(player.name) end def duplicated?(player) if (@players[player.name]) return true else return false end end def get_player(status, game_name, sente, searcher=nil) @players.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 end class Player def initialize(str, socket) @name = nil @password = nil @socket = socket @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished @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 if (@status != "finished") @status = "finished" log_message(sprintf("user %s finish", @name)) Thread::kill(@watchdog_thread) if @watchdog_thread @socket.close if (! @socket.closed?) end 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 %s +", @name, @protocol, @status, @game_name) elsif (@sente == false) return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name) elsif (@sente == nil) return sprintf("%s %s %s %s +-", @name, @protocol, @status, @game_name) end else return sprintf("%s %s %s", @name, @protocol, @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(@mytime))) begin $mutex.lock if (csa_1st_str) str = csa_1st_str csa_1st_str = nil end if (@status == "finished") return end str.chomp! if (str.class == String) case str when /^[\+\-%][^%]/, :timeout 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 Game::new(@game_name, self, rival) self.status = "agree_waiting" rival.status = "agree_waiting" end when /^%%CHAT\s+(.+)/ message = $1 LEAGUE.players.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 /^%%LIST/ buf = Array::new LEAGUE.games.each do |id, game| buf.push(sprintf("##[LIST] %s\n", id)) end buf.push("##[LIST] +OK\n") write_safe(buf.join) when /^%%SHOW\s+(\S+)/ id = $1 if (LEAGUE.games[id]) write_safe(LEAGUE.games[id].board.to_s.gsub(/^/, '##[SHOW] ')) end write_safe("##[SHOW] +OK\n") when /^%%WHO/ buf = Array::new LEAGUE.players.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") return else write_safe(sprintf("##[ERROR] unknown command %s\n", str)) end ensure $mutex.unlock end end # enf of while end end class Piece PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"} def initialize(name, sente) @name = name @sente = sente @promoted = false end attr_accessor :name, :promoted, :sente def promoted_name PROMOTE[name] end def to_s if (@sente) sg = "+" else sg = "-" end if (@promoted) n = PROMOTE[@name] else n = @name end return sg + n end end class Board def initialize @sente_hands = Array::new @gote_hands = Array::new @array = [[], [], [], [], [], [], [], [], [], []] end attr_accessor :array, :sente_hands, :gote_hands def initial @array[1][1] = Piece::new("KY", false) @array[2][1] = Piece::new("KE", false) @array[3][1] = Piece::new("GI", false) @array[4][1] = Piece::new("KI", false) @array[5][1] = Piece::new("OU", false) @array[6][1] = Piece::new("KI", false) @array[7][1] = Piece::new("GI", false) @array[8][1] = Piece::new("KE", false) @array[9][1] = Piece::new("KY", false) @array[2][2] = Piece::new("KA", false) @array[8][2] = Piece::new("HI", false) @array[1][3] = Piece::new("FU", false) @array[2][3] = Piece::new("FU", false) @array[3][3] = Piece::new("FU", false) @array[4][3] = Piece::new("FU", false) @array[5][3] = Piece::new("FU", false) @array[6][3] = Piece::new("FU", false) @array[7][3] = Piece::new("FU", false) @array[8][3] = Piece::new("FU", false) @array[9][3] = Piece::new("FU", false) @array[1][9] = Piece::new("KY", true) @array[2][9] = Piece::new("KE", true) @array[3][9] = Piece::new("GI", true) @array[4][9] = Piece::new("KI", true) @array[5][9] = Piece::new("OU", true) @array[6][9] = Piece::new("KI", true) @array[7][9] = Piece::new("GI", true) @array[8][9] = Piece::new("KE", true) @array[9][9] = Piece::new("KY", true) @array[2][8] = Piece::new("HI", true) @array[8][8] = Piece::new("KA", true) @array[1][7] = Piece::new("FU", true) @array[2][7] = Piece::new("FU", true) @array[3][7] = Piece::new("FU", true) @array[4][7] = Piece::new("FU", true) @array[5][7] = Piece::new("FU", true) @array[6][7] = Piece::new("FU", true) @array[7][7] = Piece::new("FU", true) @array[8][7] = Piece::new("FU", true) @array[9][7] = Piece::new("FU", true) end def get_piece_from_hands(hands, name) p = hands.find { |i| i.name == name } if (p) hands.delete(p) end return p end def handle_one_move(str) if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/) p = $1 x0 = $2.to_i y0 = $3.to_i x1 = $4.to_i y1 = $5.to_i name = $6 elsif (str =~ /^%/) return true else return false # illegal move end if (p == "+") sente = true hands = @sente_hands else sente = false hands = @gote_hands end if (@array[x1][y1]) if (@array[x1][y1] == sente) # this is mine return false end hands.push(@array[x1][y1]) @array[x1][y1] = nil end if ((x0 == 0) && (y0 == 0)) p = get_piece_from_hands(hands, name) return false if (! p) # i don't have this one @array[x1][y1] = p p.sente = sente p.promoted = false else @array[x1][y1] = @array[x0][y0] @array[x0][y0] = nil if (@array[x1][y1].name != name) # promoted ? return false if (@array[x1][y1].promoted_name != name) # can't promote @array[x1][y1].promoted = true end end return true # legal move end def to_s a = Array::new y = 1 while (y <= 9) a.push(sprintf("P%d", y)) x = 9 while (x >= 1) piece = @array[x][y] if (piece) s = piece.to_s else s = " * " end a.push(s) x = x - 1 end a.push(sprintf("\n")) y = y + 1 end if (! sente_hands.empty?) a.push("P+") sente_hands.each do |p| a.push("00" + p.name) end a.push("\n") end if (! gote_hands.empty?) a.push("P-") gote_hands.each do |p| a.push("00" + p.name) end a.push("\n") end return a.join end 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")) LEAGUE.games[@id] = self log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name)) @logfile = @id + ".csa" @board = Board::new @board.initial @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.game = nil @gote.game = nil @sente.status = "connected" @gote.status = "connected" if (@current_player.protocol == "CSA") @current_player.finish end if (@next_player.protocol == "CSA") @next_player.finish end LEAGUE.games.delete(@id) 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) legal_move = true if (str != :timeout) legal_move = @board.handle_one_move(str) if (legal_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) else @fh.printf("'ILLEGAL_MOVE(%s)\n", str) end @current_player.mytime = @current_player.mytime - t else @current_player.mytime = 0 end if (!legal_move) illegal_end() finish_flag = true elsif (@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 return finish_flag end end def illegal_end @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") 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 <= 4) client.close Thread::kill(Thread::current) end LEAGUE.add(player) break else client.write_safe("LOGIN:incorrect" + eol) client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4) client.close Thread::kill(Thread::current) end ensure $mutex.unlock end end # login loop log_message(sprintf("user %s login", player.name)) player.run begin $mutex.lock if (player.game) player.game.finish end if (player.status != "finished") player.finish end LEAGUE.delete(player) log_message(sprintf("user %s logout", player.name)) ensure $mutex.unlock end end end end if ($0 == __FILE__) main end