#! /usr/bin/env ruby ## $Id$ ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch) ## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org) ## ## 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 require 'kconv' require 'getoptlong' require 'thread' require 'timeout' require 'socket' require 'yaml' require 'yaml/store' require 'digest/md5' require 'webrick' require 'fileutils' def gets_safe(socket, timeout=nil) if r = select([socket], nil, nil, timeout) return r[0].first.gets else return :timeout end rescue Exception => ex log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") return :exception end module ShogiServer # for a namespace Max_Identifier_Length = 32 Default_Timeout = 60 # for single socket operation Default_Game_Name = "default-1500-0" One_Time = 10 Least_Time_Per_Move = 1 Login_Time = 300 # time for LOGIN Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.') Release.concat("-") if (Release == "") Revision = "$Revision$".gsub(/[^\.\d]/, '') class League class Floodgate class << self def game_name?(str) return /^floodgate-\d+-\d+$/.match(str) ? true : false end end def initialize(league) @league = league @next_time = nil charge end def run @thread = Thread.new do Thread.pass while (true) begin sleep(10) next if Time.now < @next_time @league.reload match_game charge rescue Exception => ex # ignore errors log_error("[in Floodgate's thread] #{ex}") end end end end def shutdown @thread.kill if @thread end # private def charge now = Time.now if now.min < 30 @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30) else @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600 end # for test # if now.sec < 30 # @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30) # else # @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60 # end end def match_game players = @league.find_all_players do |pl| pl.status == "game_waiting" && Floodgate.game_name?(pl.game_name) && pl.sente == nil end log_warning("DEBUG: %s" % [File.join(File.dirname(__FILE__), "pairing.rb")]) load File.join(File.dirname(__FILE__), "pairing.rb") Pairing.default_pairing.match(players) end end # class Floodgate def initialize @mutex = Mutex.new # guard @players @games = Hash::new @players = Hash::new @event = nil @dir = File.dirname(__FILE__) @floodgate = Floodgate.new(self) @floodgate.run end attr_accessor :players, :games, :event, :dir def shutdown @mutex.synchronize do @players.each {|a| save(a)} end @floodgate.shutdown end # this should be called just after instanciating a League object. def setup_players_database @db = YAML::Store.new(File.join(@dir, "players.yaml")) end def add(player) self.load(player) if player.player_id @mutex.synchronize do @players[player.name] = player end end def delete(player) @mutex.synchronize do save(player) @players.delete(player.name) end end def reload @mutex.synchronize do @players.each {|player| load(player)} end end def find_all_players found = nil @mutex.synchronize do found = @players.find_all do |name, player| yield player end end return found.map {|a| a.last} end def find(player_name) found = nil @mutex.synchronize do found = @players[player_name] end return found end def get_player(status, game_name, sente, searcher) found = nil @mutex.synchronize do found = @players.find do |name, player| (player.status == status) && (player.game_name == game_name) && ( (sente == nil) || (player.sente == nil) || (player.sente == sente) ) && (player.name != searcher.name) end end return found ? found.last : nil end def load(player) hash = search(player.player_id) return unless hash # a current user player.name = hash['name'] player.rate = hash['rate'] || 0 player.modified_at = hash['last_modified'] player.rating_group = hash['rating_group'] player.win = hash['win'] || 0 player.loss = hash['loss'] || 0 player.last_game_win = hash['last_game_win'] || false end def save(player) @db.transaction do break unless @db["players"] @db["players"].each do |group, players| hash = players[player.player_id] if hash hash['last_game_win'] = player.last_game_win break end end end end def search(player_id) hash = nil @db.transaction(true) do break unless @db["players"] @db["players"].each do |group, players| hash = players[player_id] break if hash end end hash end def rated_players players = [] @db.transaction(true) do break unless @db["players"] @db["players"].each do |group, players_hash| players << players_hash.keys end end return players.flatten.collect do |player_id| p = BasicPlayer.new p.player_id = player_id self.load(p) p end end end ###################################################### # Processes the LOGIN command. # class Login def Login.good_login?(str) tokens = str.split if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) && (tokens[0] == "LOGIN") && (good_identifier?(tokens[1]))) return true else return false end end def Login.good_game_name?(str) if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1))) return true else return false end end def Login.good_identifier?(str) if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/ return true else return false end end def Login.factory(str, player) (login, player.name, password, ext) = str.chomp.split if ext return Loginx1.new(player, password) else return LoginCSA.new(player, password) end end attr_reader :player # the first command that will be executed just after LOGIN. # If it is nil, the default process will be started. attr_reader :csa_1st_str def initialize(player, password) @player = player @csa_1st_str = nil parse_password(password) end def process @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name)) log_message(sprintf("user %s run in %s mode", @player.name, @player.protocol)) end def incorrect_duplicated_player(str) @player.write_safe("LOGIN:incorrect\n") @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4) sleep 3 # wait for sending the above messages. @player.name = "%s [duplicated]" % [@player.name] @player.finish end end ###################################################### # Processes LOGIN for the CSA standard mode. # class LoginCSA < Login PROTOCOL = "CSA" def initialize(player, password) @gamename = nil super @player.protocol = PROTOCOL end def parse_password(password) if Login.good_game_name?(password) @gamename = password @player.set_password(nil) elsif password.split(",").size > 1 @gamename, *trip = password.split(",") @player.set_password(trip.join(",")) else @player.set_password(password) @gamename = Default_Game_Name end @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name end def process super @csa_1st_str = "%%GAME #{@gamename} *" end end ###################################################### # Processes LOGIN for the extented mode. # class Loginx1 < Login PROTOCOL = "x1" def initialize(player, password) super @player.protocol = PROTOCOL end def parse_password(password) @player.set_password(password) end def process super @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL)) end end class BasicPlayer def initialize @player_id = nil @name = nil @password = nil @last_game_win = false end # Idetifier of the player in the rating system attr_accessor :player_id # Name of the player attr_accessor :name # Password of the player, which does not include a trip attr_accessor :password # Score in the rating sysem attr_accessor :rate # Number of games for win and loss in the rating system attr_accessor :win, :loss # Group in the rating system attr_accessor :rating_group # Last timestamp when the rate was modified attr_accessor :modified_at # Whether win the previous game or not attr_accessor :last_game_win def modified_at @modified_at || Time.now end def rate=(new_rate) if @rate != new_rate @rate = new_rate @modified_at = Time.now end end def rated? @player_id != nil end def last_game_win? return @last_game_win end def simple_player_id if @trip simple_name = @name.gsub(/@.*?$/, '') "%s+%s" % [simple_name, @trip[0..8]] else @name end end ## # Parses str in the LOGIN command, sets up @player_id and @trip # def set_password(str) if str && !str.empty? @password = str.strip @player_id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)] else @player_id = @password = nil end end end class Player < BasicPlayer def initialize(str, socket, eol=nil) super() @socket = socket @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished @protocol = nil # CSA or x1 @eol = eol || "\m" # favorite eol code @game = nil @game_name = "" @mytime = 0 # set in start method also @sente = nil @socket_buffer = [] @main_thread = Thread::current @mutex_write_guard = Mutex.new end attr_accessor :socket, :status attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente attr_accessor :main_thread attr_reader :socket_buffer def kill log_message(sprintf("user %s killed", @name)) if (@game) @game.kill(self) end finish Thread::kill(@main_thread) if @main_thread end def finish if (@status != "finished") @status = "finished" log_message(sprintf("user %s finish", @name)) begin # @socket.close if (! @socket.closed?) rescue log_message(sprintf("user %s finish failed", @name)) end end end def write_safe(str) @mutex_write_guard.synchronize do begin if @socket.closed? log_warning("%s's socket has been closed." % [@name]) return end if r = select(nil, [@socket], nil, 20) r[1].first.write(str) else log_error("Sending a message to #{@name} timed up.") end rescue Exception => ex log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}") end end end def to_s if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status) 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 run(csa_1st_str=nil) while ( csa_1st_str || str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) ) $mutex.lock begin if (@game && @game.turn?(self)) @socket_buffer << str str = @socket_buffer.shift end log_message("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")]) if $DEBUG if (csa_1st_str) str = csa_1st_str csa_1st_str = nil end if (@status == "finished") return end str.chomp! if (str.class == String) # may be strip! ? case str when "" # Application-level protocol for Keep-Alive # If the server gets LF, it sends back LF. # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet. write_safe("\n") when /^[\+\-][^%]/ if (@status == "game") array_str = str.split(",") move = array_str.shift additional = array_str.shift if /^'(.*)/ =~ additional comment = array_str.unshift("'*#{$1.toeuc}") end s = @game.handle_one_move(move, self) @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s) return if (s && @protocol == LoginCSA::PROTOCOL) end when /^%[^%]/, :timeout if (@status == "game") s = @game.handle_one_move(str, self) return if (s && @protocol == LoginCSA::PROTOCOL) # else # begin # @socket.write("##[KEEPALIVE] #{Time.now}\n") # rescue Exception => ex # log_error("Failed to send a keepalive to #{@name}.") # log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") # return # end end when :exception log_error("Failed to receive a message from #{@name}.") return when /^REJECT/ if (@status == "agree_waiting") @game.reject(@name) return if (@protocol == LoginCSA::PROTOCOL) else write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)) 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(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)) end when /^%%SHOW\s+(\S+)/ game_id = $1 if (LEAGUE.games[game_id]) write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] ')) end write_safe("##[SHOW] +OK\n") when /^%%MONITORON\s+(\S+)/ game_id = $1 if (LEAGUE.games[game_id]) LEAGUE.games[game_id].monitoron(self) write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] ")) write_safe("##[MONITOR][#{game_id}] +OK\n") end when /^%%MONITOROFF\s+(\S+)/ game_id = $1 if (LEAGUE.games[game_id]) LEAGUE.games[game_id].monitoroff(self) end when /^%%HELP/ write_safe( %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!) when /^%%RATING/ players = LEAGUE.rated_players players.sort {|a,b| b.rate <=> a.rate}.each do |p| write_safe("##[RATING] %s \t %4d @%s\n" % [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")]) end write_safe("##[RATING] +OK\n") when /^%%VERSION/ write_safe "##[VERSION] Shogi Server revision #{Revision}\n" write_safe("##[VERSION] +OK\n") when /^%%GAME\s*$/ if ((@status == "connected") || (@status == "game_waiting")) @status = "connected" @game_name = "" else write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status)) end when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/ command_name = $1 game_name = $2 my_sente_str = $3 if (! Login::good_game_name?(game_name)) write_safe(sprintf("##[ERROR] bad game name\n")) next elsif ((@status == "connected") || (@status == "game_waiting")) ## continue else write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status)) next end rival = nil if (League::Floodgate.game_name?(game_name)) if (my_sente_str != "*") write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", my_sente_str, game_name)) next end @sente = nil else if (my_sente_str == "*") rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference elsif (my_sente_str == "+") rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote elsif (my_sente_str == "-") rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente else ## never reached write_safe(sprintf("##[ERROR] bad game option\n")) next end end if (rival) @game_name = game_name if ((my_sente_str == "*") && (rival.sente == nil)) if (rand(2) == 0) @sente = true rival.sente = false else @sente = false rival.sente = true end elsif (rival.sente == true) # rival has higher priority @sente = false elsif (rival.sente == false) @sente = true elsif (my_sente_str == "+") @sente = true rival.sente = false elsif (my_sente_str == "-") @sente = false rival.sente = true else ## never reached end Game::new(@game_name, self, rival) else # rival not found if (command_name == "GAME") @status = "game_waiting" @game_name = game_name if (my_sente_str == "+") @sente = true elsif (my_sente_str == "-") @sente = false else @sente = nil end else # challenge write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name)) @status = "connected" @game_name = "" @sente = nil end end when /^%%CHAT\s+(.+)/ message = $1 LEAGUE.players.each do |name, player| if (player.protocol != LoginCSA::PROTOCOL) player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 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 /^%%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/ @status = "connected" write_safe("LOGOUT:completed\n") return when /^CHALLENGE/ # This command is only available for CSA's official testing server. # So, this means nothing for this program. write_safe("CHALLENGE ACCEPTED\n") when /^\s*$/ ## ignore null string else msg = "##[ERROR] unknown command %s\n" % [str] write_safe(msg) log_error(msg) end ensure $mutex.unlock end end # enf of while end # def run end # class class Piece PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"} def initialize(board, x, y, sente, promoted=false) @board = board @x = x @y = y @sente = sente @promoted = promoted if ((x == 0) || (y == 0)) if (sente) hands = board.sente_hands else hands = board.gote_hands end hands.push(self) hands.sort! {|a, b| a.name <=> b.name } else @board.array[x][y] = self end end attr_accessor :promoted, :sente, :x, :y, :board def room_of_head?(x, y, name) true end def movable_grids return adjacent_movable_grids + far_movable_grids end def far_movable_grids return [] end def jump_to?(x, y) if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9)) if ((@board.array[x][y] == nil) || # dst is empty (@board.array[x][y].sente != @sente)) # dst is enemy return true end end return false end def put_to?(x, y) if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9)) if (@board.array[x][y] == nil) # dst is empty? return true end end return false end def adjacent_movable_grids grids = Array::new if (@promoted) moves = @promoted_moves else moves = @normal_moves end moves.each do |(dx, dy)| if (@sente) cand_y = @y - dy else cand_y = @y + dy end cand_x = @x + dx if (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) end end return grids end def move_to?(x, y, name) return false if (! room_of_head?(x, y, name)) return false if ((name != @name) && (name != @promoted_name)) return false if (@promoted && (name != @promoted_name)) # can't un-promote if (! @promoted) return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece if (@sente) return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote else return false if ((6 >= @y) && (6 >= y) && (name != @name)) end end if ((@x == 0) || (@y == 0)) return jump_to?(x, y) else return movable_grids.include?([x, y]) end end def move_to(x, y) if ((@x == 0) || (@y == 0)) if (@sente) @board.sente_hands.delete(self) else @board.gote_hands.delete(self) end @board.array[x][y] = self elsif ((x == 0) || (y == 0)) @promoted = false # clear promoted flag before moving to hands if (@sente) @board.sente_hands.push(self) else @board.gote_hands.push(self) end @board.array[@x][@y] = nil else @board.array[@x][@y] = nil @board.array[x][y] = self end @x = x @y = y end def point @point end def name @name end def promoted_name @promoted_name end def to_s if (@sente) sg = "+" else sg = "-" end if (@promoted) n = @promoted_name else n = @name end return sg + n end end class PieceFU < Piece def initialize(*arg) @point = 1 @normal_moves = [[0, +1]] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "FU" @promoted_name = "TO" super end def room_of_head?(x, y, name) if (name == "FU") if (@sente) return false if (y == 1) else return false if (y == 9) end ## 2fu check c = 0 iy = 1 while (iy <= 9) if ((iy != @y) && # not source position @board.array[x][iy] && (@board.array[x][iy].sente == @sente) && # mine (@board.array[x][iy].name == "FU") && (@board.array[x][iy].promoted == false)) return false end iy = iy + 1 end end return true end end class PieceKY < Piece def initialize(*arg) @point = 1 @normal_moves = [] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "KY" @promoted_name = "NY" super end def room_of_head?(x, y, name) if (name == "KY") if (@sente) return false if (y == 1) else return false if (y == 9) end end return true end def far_movable_grids grids = Array::new if (@promoted) return [] else if (@sente) # up cand_x = @x cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y - 1 end else # down cand_x = @x cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y + 1 end end return grids end end end class PieceKE < Piece def initialize(*arg) @point = 1 @normal_moves = [[+1, +2], [-1, +2]] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "KE" @promoted_name = "NK" super end def room_of_head?(x, y, name) if (name == "KE") if (@sente) return false if ((y == 1) || (y == 2)) else return false if ((y == 9) || (y == 8)) end end return true end end class PieceGI < Piece def initialize(*arg) @point = 1 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "GI" @promoted_name = "NG" super end end class PieceKI < Piece def initialize(*arg) @point = 1 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @promoted_moves = [] @name = "KI" @promoted_name = nil super end end class PieceKA < Piece def initialize(*arg) @point = 5 @normal_moves = [] @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]] @name = "KA" @promoted_name = "UM" super end def far_movable_grids grids = Array::new ## up right cand_x = @x - 1 cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x - 1 cand_y = cand_y - 1 end ## down right cand_x = @x - 1 cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x - 1 cand_y = cand_y + 1 end ## up left cand_x = @x + 1 cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x + 1 cand_y = cand_y - 1 end ## down left cand_x = @x + 1 cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x + 1 cand_y = cand_y + 1 end return grids end end class PieceHI < Piece def initialize(*arg) @point = 5 @normal_moves = [] @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]] @name = "HI" @promoted_name = "RY" super end def far_movable_grids grids = Array::new ## up cand_x = @x cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y - 1 end ## down cand_x = @x cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y + 1 end ## right cand_x = @x - 1 cand_y = @y while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x - 1 end ## down cand_x = @x + 1 cand_y = @y while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x + 1 end return grids end end class PieceOU < Piece def initialize(*arg) @point = 0 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]] @promoted_moves = [] @name = "OU" @promoted_name = nil super end end class Board def initialize @sente_hands = Array::new @gote_hands = Array::new @history = Hash::new(0) @sente_history = Hash::new(0) @gote_history = Hash::new(0) @array = [[], [], [], [], [], [], [], [], [], []] @move_count = 0 @teban = nil # black => true, white => false end attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history attr_reader :move_count def initial PieceKY::new(self, 1, 1, false) PieceKE::new(self, 2, 1, false) PieceGI::new(self, 3, 1, false) PieceKI::new(self, 4, 1, false) PieceOU::new(self, 5, 1, false) PieceKI::new(self, 6, 1, false) PieceGI::new(self, 7, 1, false) PieceKE::new(self, 8, 1, false) PieceKY::new(self, 9, 1, false) PieceKA::new(self, 2, 2, false) PieceHI::new(self, 8, 2, false) (1..9).each do |i| PieceFU::new(self, i, 3, false) end PieceKY::new(self, 1, 9, true) PieceKE::new(self, 2, 9, true) PieceGI::new(self, 3, 9, true) PieceKI::new(self, 4, 9, true) PieceOU::new(self, 5, 9, true) PieceKI::new(self, 6, 9, true) PieceGI::new(self, 7, 9, true) PieceKE::new(self, 8, 9, true) PieceKY::new(self, 9, 9, true) PieceKA::new(self, 8, 8, true) PieceHI::new(self, 2, 8, true) (1..9).each do |i| PieceFU::new(self, i, 7, true) end @teban = true end def have_piece?(hands, name) piece = hands.find { |i| i.name == name } return piece end def move_to(x0, y0, x1, y1, name, sente) if (sente) hands = @sente_hands else hands = @gote_hands end if ((x0 == 0) || (y0 == 0)) piece = have_piece?(hands, name) return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece? piece.move_to(x1, y1) else return :illegal if (! @array[x0][y0].move_to?(x1, y1, name)) # TODO null check? if (@array[x0][y0].name != name) # promoted ? @array[x0][y0].promoted = true end if (@array[x1][y1]) # capture if (@array[x1][y1].name == "OU") return :outori # return board update end @array[x1][y1].sente = @array[x0][y0].sente @array[x1][y1].move_to(0, 0) hands.sort! {|a, b| # TODO refactor. Move to Piece class a.name <=> b.name } end @array[x0][y0].move_to(x1, y1) end @move_count += 1 @teban = @teban ? false : true return true end def look_for_ou(sente) x = 1 while (x <= 9) y = 1 while (y <= 9) if (@array[x][y] && (@array[x][y].name == "OU") && (@array[x][y].sente == sente)) return @array[x][y] end y = y + 1 end x = x + 1 end raise "can't find ou" end # note checkmate, but check. sente is checked. def checkmated?(sente) # sente is loosing ou = look_for_ou(sente) x = 1 while (x <= 9) y = 1 while (y <= 9) if (@array[x][y] && (@array[x][y].sente != sente)) if (@array[x][y].movable_grids.include?([ou.x, ou.y])) return true end end y = y + 1 end x = x + 1 end return false end def uchifuzume?(sente) rival_ou = look_for_ou(! sente) # rival's ou if (sente) # rival is gote if ((rival_ou.y != 9) && (@array[rival_ou.x][rival_ou.y + 1]) && (@array[rival_ou.x][rival_ou.y + 1].name == "FU") && (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true fu_x = rival_ou.x fu_y = rival_ou.y + 1 else return false end else # gote if ((rival_ou.y != 1) && (@array[rival_ou.x][rival_ou.y - 1]) && (@array[rival_ou.x][rival_ou.y - 1].name == "FU") && (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true fu_x = rival_ou.x fu_y = rival_ou.y - 1 else return false end end ## case: rival_ou is moving rival_ou.movable_grids.each do |(cand_x, cand_y)| tmp_board = Marshal.load(Marshal.dump(self)) s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente) raise "internal error" if (s != true) if (! tmp_board.checkmated?(! sente)) # good move return false end end ## case: rival is capturing fu x = 1 while (x <= 9) y = 1 while (y <= 9) if (@array[x][y] && (@array[x][y].sente != sente) && @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable names = [] if (@array[x][y].promoted) names << @array[x][y].promoted_name else names << @array[x][y].name if @array[x][y].promoted_name && @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name) names << @array[x][y].promoted_name end end names.map! do |name| tmp_board = Marshal.load(Marshal.dump(self)) s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente) if s == :illegal s # result else tmp_board.checkmated?(! sente) # result end end all_illegal = names.find {|a| a != :illegal} raise "internal error: legal move not found" if all_illegal == nil r = names.find {|a| a == false} # good move return false if r == false # found good move end y = y + 1 end x = x + 1 end return true end # @[sente|gote]_history has at least one item while the player is checking the other or # the other escapes. def update_sennichite(player) str = to_s @history[str] += 1 if checkmated?(!player) if (player) @sente_history["dummy"] = 1 # flag to see Sente player is checking Gote player else @gote_history["dummy"] = 1 # flag to see Gote player is checking Sente player end else if (player) @sente_history.clear # no more continuous check else @gote_history.clear # no more continuous check end end if @sente_history.size > 0 # possible for Sente's or Gote's turn @sente_history[str] += 1 end if @gote_history.size > 0 # possible for Sente's or Gote's turn @gote_history[str] += 1 end end def oute_sennichite?(player) if (@sente_history[to_s] >= 4) return :oute_sennichite_sente_lose elsif (@gote_history[to_s] >= 4) return :oute_sennichite_gote_lose else return nil end end def sennichite?(sente) if (@history[to_s] >= 4) # already 3 times return true end return false end def good_kachi?(sente) if (checkmated?(sente)) puts "'NG: Checkmating." if $DEBUG return false end ou = look_for_ou(sente) if (sente && (ou.y >= 4)) puts "'NG: Black's OU does not enter yet." if $DEBUG return false end if (! sente && (ou.y <= 6)) puts "'NG: White's OU does not enter yet." if $DEBUG return false end number = 0 point = 0 if (sente) hands = @sente_hands r = [1, 2, 3] else hands = @gote_hands r = [7, 8, 9] end r.each do |y| x = 1 while (x <= 9) if (@array[x][y] && (@array[x][y].sente == sente) && (@array[x][y].point > 0)) point = point + @array[x][y].point number = number + 1 end x = x + 1 end end hands.each do |piece| point = point + piece.point end if (number < 10) puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG return false end if (sente) if (point < 28) puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG return false end else if (point < 27) puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG return false end end puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG return true end # sente is nil only if tests in test_board run def handle_one_move(str, sente=nil) if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/) sg = $1 x0 = $2.to_i y0 = $3.to_i x1 = $4.to_i y1 = $5.to_i name = $6 elsif (str =~ /^%KACHI/) raise ArgumentError, "sente is null", caller if sente == nil if (good_kachi?(sente)) return :kachi_win else return :kachi_lose end elsif (str =~ /^%TORYO/) return :toryo else return :illegal end if (((x0 == 0) || (y0 == 0)) && # source is not from hand ((x0 != 0) || (y0 != 0))) return :illegal elsif ((x1 == 0) || (y1 == 0)) # destination is out of board return :illegal end if (sg == "+") sente = true if sente == nil # deprecated return :illegal unless sente == true # black player's move must be black hands = @sente_hands else sente = false if sente == nil # deprecated return :illegal unless sente == false # white player's move must be white hands = @gote_hands end ## source check if ((x0 == 0) && (y0 == 0)) return :illegal if (! have_piece?(hands, name)) elsif (! @array[x0][y0]) return :illegal # no piece elsif (@array[x0][y0].sente != sente) return :illegal # this is not mine elsif (@array[x0][y0].name != name) return :illegal if (@array[x0][y0].promoted_name != name) # can't promote end ## destination check if (@array[x1][y1] && (@array[x1][y1].sente == sente)) # can't capture mine return :illegal elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1]) return :illegal # can't put on existing piece end tmp_board = Marshal.load(Marshal.dump(self)) return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal) return :oute_kaihimore if (tmp_board.checkmated?(sente)) tmp_board.update_sennichite(sente) os_result = tmp_board.oute_sennichite?(sente) return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose return :sennichite if tmp_board.sennichite?(sente) if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente)) return :uchifuzume end move_to(x0, y0, x1, y1, name, sente) update_sennichite(sente) return :normal 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 a.push("%s\n" % [@teban ? "+" : "-"]) return a.join end end class GameResult attr_reader :players, :black, :white def initialize(p1, p2) @players = [p1, p2] if p1.sente && !p2.sente @black, @white = p1, p2 elsif !p1.sente && p2.sente @black, @white = p2, p1 else raise "Never reached!" end end end class GameResultWin < GameResult attr_reader :winner, :loser def initialize(winner, loser) super @winner, @loser = winner, loser @winner.last_game_win = true @loser.last_game_win = false end def to_s black_name = @black.player_id || @black.name white_name = @white.player_id || @white.name "%s:%s" % [black_name, white_name] end end class GameResultDraw < GameResult def initialize(p1, p2) super p1.last_game_win = false p2.last_game_win = false end end class Game @@mutex = Mutex.new @@time = 0 def initialize(game_name, player0, player1) @monitors = Array::new @game_name = game_name if (@game_name =~ /-(\d+)-(\d+)$/) @total_time = $1.to_i @byoyomi = $2.to_i end if (player0.sente) @sente, @gote = player0, player1 else @sente, @gote = player1, player0 end @sente.socket_buffer.clear @gote.socket_buffer.clear @current_player, @next_player = @sente, @gote @sente.game = self @gote.game = self @last_move = "" @current_turn = 0 @sente.status = "agree_waiting" @gote.status = "agree_waiting" @game_id = sprintf("%s+%s+%s+%s+%s", LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time) @logfile = File.join(LEAGUE.dir, @game_id + ".csa") LEAGUE.games[@game_id] = self log_message(sprintf("game created %s", @game_id)) @board = Board::new @board.initial @start_time = nil @fh = nil @result = nil propose end attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors attr_accessor :last_move, :current_turn attr_reader :result def rated? @sente.rated? && @gote.rated? end def turn?(player) return player.status == "game" && @current_player == player end def monitoron(monitor) @monitors.delete(monitor) @monitors.push(monitor) end def monitoroff(monitor) @monitors.delete(monitor) end def reject(rejector) @sente.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector)) @gote.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector)) finish end def kill(killer) if ["agree_waiting", "start_waiting"].include?(@sente.status) reject(killer.name) elsif (@current_player == killer) abnormal_lose() finish end end def finish log_message(sprintf("game finished %s", @game_id)) @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 == LoginCSA::PROTOCOL) @current_player.finish end if (@next_player.protocol == LoginCSA::PROTOCOL) @next_player.finish end @monitors = Array::new @sente = nil @gote = nil @current_player = nil @next_player = nil LEAGUE.games.delete(@game_id) end # class Game def handle_one_move(str, player) unless turn?(player) return false if str == :timeout @fh.puts("'Deferred %s" % [str]) log_warning("Deferred a move [%s] scince it is not %s 's turn." % [str, player.name]) player.socket_buffer << str # always in the player's thread return nil end finish_flag = true @end_time = Time::new t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max move_status = nil if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0))) status = :timeout elsif (str == :timeout) return false # time isn't expired. players aren't swapped. continue game else @current_player.mytime -= t if (@current_player.mytime < 0) @current_player.mytime = 0 end move_status = @board.handle_one_move(str, @sente == @current_player) if [:illegal, :uchifuzme, :oute_kaihimore].include?(move_status) @fh.printf("'ILLEGAL_MOVE(%s)\n", str) else if [:normal, :outori, :sennichite, :oute_sennichite_sente_lose, :oute_sennichite_gote_lose].include?(move_status) # Thinking time includes network traffic @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) @last_move = sprintf("%s,T%d", str, t) @current_turn += 1 end @monitors.each do |monitor| monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@game_id}] ")) monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @game_id)) end end end if (@next_player.status != "game") # rival is logout or disconnected abnormal_win() elsif (status == :timeout) timeout_lose() elsif (move_status == :illegal) illegal_lose() elsif (move_status == :kachi_win) kachi_win() elsif (move_status == :kachi_lose) kachi_lose() elsif (move_status == :toryo) toryo_lose() elsif (move_status == :outori) outori_win() elsif (move_status == :oute_sennichite_sente_lose) oute_sennichite_win_lose(@gote, @sente) # Sente is checking elsif (move_status == :oute_sennichite_gote_lose) oute_sennichite_win_lose(@sente, @gote) # Gote is checking elsif (move_status == :sennichite) sennichite_draw() elsif (move_status == :uchifuzume) uchifuzume_lose() elsif (move_status == :oute_kaihimore) oute_kaihimore_lose() else finish_flag = false end finish() if finish_flag @current_player, @next_player = @next_player, @current_player @start_time = Time::new return finish_flag end def abnormal_win @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") @fh.printf("%%TORYO\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id)) end end def abnormal_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") @fh.printf("%%TORYO\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id)) end end def sennichite_draw @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#SENNICHITE\n#DRAW\n") @next_player.write_safe("#SENNICHITE\n#DRAW\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name) @result = GameResultDraw.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @game_id)) end end def oute_sennichite_win_lose(winner, loser) @current_player.status = "connected" @next_player.status = "connected" loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n") winner.write_safe("#OUTE_SENNICHITE\n#WIN\n") @fh.print(@board.to_s.gsub(/^/, "\'")) if loser == @current_player @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name) else @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name) end @result = GameResultWin.new(winner, loser) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @game_id)) end end def illegal_lose @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") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id)) end end def uchifuzume_lose @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") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id)) end end def oute_kaihimore_lose @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") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id)) end end def timeout_lose @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") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @game_id)) end end def kachi_win @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n") @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n") @fh.printf("%%KACHI\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @game_id)) end end def kachi_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n") @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n") @fh.printf("%%KACHI\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @game_id)) end end def toryo_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") @fh.printf("%%TORYO\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id)) end end def outori_win @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id)) end end def start log_message(sprintf("game started %s", @game_id)) @sente.write_safe(sprintf("START:%s\n", @game_id)) @gote.write_safe(sprintf("START:%s\n", @game_id)) @sente.mytime = @total_time @gote.mytime = @total_time @start_time = Time::new end def propose @fh = open(@logfile, "w") @fh.sync = true @fh.puts("V2") @fh.puts("N+#{@sente.name}") @fh.puts("N-#{@gote.name}") @fh.puts("$EVENT:#{@game_id}") @sente.write_safe(propose_message("+")) @gote.write_safe(propose_message("-")) now = Time::new.strftime("%Y/%m/%d %H:%M:%S") @fh.puts("$START_TIME:#{now}") @fh.print <= 4) end ensure $mutex.unlock end end # login loop return [player, login] end def main $mutex = Mutex::new Thread::start do Thread.pass mutex_watchdog($mutex, 10) end $options = parse_command_line if (ARGV.length != 2) usage exit 2 end LEAGUE.event = ARGV.shift port = ARGV.shift dir = $options["daemon"] dir = File.expand_path(dir) if dir if dir && ! File.exist?(dir) FileUtils.mkdir(dir) end log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT $logger = WEBrick::Log.new(log_file) # thread safe LEAGUE.dir = dir || File.dirname(__FILE__) LEAGUE.setup_players_database config = {} config[:Port] = port config[:ServerType] = WEBrick::Daemon if $options["daemon"] config[:Logger] = $logger if $options["pid-file"] pid_file = File.expand_path($options["pid-file"]) config[:StartCallback] = Proc.new do write_pid_file(pid_file) end config[:StopCallback] = Proc.new do FileUtils.rm(pid_file, :force => true) end end server = WEBrick::GenericServer.new(config) ["INT", "TERM"].each do |signal| trap(signal) do LEAGUE.shutdown server.shutdown end end $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] log_message("server started [Revision: #{ShogiServer::Revision}]") server.start do |client| # client.sync = true # this is already set in WEBrick client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time player, login = login_loop(client) # loop next unless player log_message(sprintf("user %s login", player.name)) login.process player.run(login.csa_1st_str) # loop begin $mutex.lock if (player.game) player.game.kill(player) end player.finish # socket has been closed LEAGUE.delete(player) log_message(sprintf("user %s logout", player.name)) ensure $mutex.unlock end end end if ($0 == __FILE__) STDOUT.sync = true STDERR.sync = true TCPSocket.do_not_reverse_lookup = true Thread.abort_on_exception = $DEBUG ? true : false LEAGUE = ShogiServer::League::new main end