X-Git-Url: http://git.sourceforge.jp/view?p=shogi-server%2Fshogi-server.git;a=blobdiff_plain;f=shogi-server;h=0cde3d20dd40230110a476d5f5a7e210610e622a;hp=4782f70692c82d2ae81586782982a6d1e983c9d2;hb=e76b74b545d3350b3c2215b7b108327b825f8bc5;hpb=90fbed0338fc9d67c7474dfa698b5ffe84c7d183 diff --git a/shogi-server b/shogi-server index 4782f70..0cde3d2 100755 --- a/shogi-server +++ b/shogi-server @@ -17,6 +17,8 @@ ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +module ShogiServer # for a namespace + Max_Write_Queue_Size = 1000 Max_Identifier_Length = 32 Default_Timeout = 60 # for single socket operation @@ -31,15 +33,14 @@ 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 'yaml' +require 'yaml/store' +require 'digest/md5' -TCPSocket.do_not_reverse_lookup = true class TCPSocket @@ -88,32 +89,255 @@ class League @games = Hash::new @players = Hash::new @event = nil + @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") ) end attr_accessor :players, :games, :event def add(player) + self.load(player) if player.id @players[player.name] = player end + def delete(player) @players.delete(player.name) 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)) && + ((sente == nil) || (player.sente == nil) || (player.sente == sente)) && ((searcher == nil) || (player != searcher))) return player end end return nil end + + def load(player) + hash = search(player.id) + if hash + # a current user + player.name = hash['name'] + player.rate = hash['rate'] + player.modified_at = hash['last_modified'] + player.rating_group = hash['rating_group'] + end + end + + def search(id) + hash = nil + @db.transaction do + @db["players"].each do |group, players| + hash = players[id] + break if hash + end + end + hash + end + + def rated_players + players = [] + @db.transaction(true) do + @db["players"].each do |group, players_hash| + players << players_hash.keys + end + end + return players.flatten.collect do |id| + p = BasicPlayer.new + p.id = id + self.load(p) + p + end + end end -class Player - def initialize(str, socket) + +###################################################### +# 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 + # Idetifier of the player in the rating system + attr_accessor :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 + + # Group in the rating system + attr_accessor :rating_group + + # Last timestamp when the rate was modified + attr_accessor :modified_at + + def initialize @name = nil @password = nil + end + + 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? + @id != nil + end + + def simple_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 @id and @trip + # + def set_password(str) + if str && !str.empty? + @password = str.strip + @id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)] + else + @id = @password = nil + end + end +end + + +class Player < BasicPlayer + def initialize(str, socket) + super() @socket = socket @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished @@ -123,15 +347,18 @@ class Player @game_name = "" @mytime = 0 # set in start method also @sente = nil - @writer_thread = nil - @main_thread = nil @write_queue = Queue::new - login(str) + @main_thread = Thread::current + @writer_thread = Thread::start do + Thread.pass + writer() + end end - attr_accessor :name, :password, :socket, :status + attr_accessor :socket, :status attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente attr_accessor :main_thread, :writer_thread, :write_queue + def kill log_message(sprintf("user %s killed", @name)) if (@game) @@ -145,6 +372,7 @@ class Player if (@status != "finished") @status = "finished" log_message(sprintf("user %s finish", @name)) + # TODO you should confirm that there is no message in the queue. Thread::kill(@writer_thread) if @writer_thread begin @socket.close if (! @socket.closed?) @@ -185,32 +413,7 @@ class Player @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"') 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 - @main_thread = Thread::current - @writer_thread = Thread::start do - writer() - 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_Game_Name} *" - end - + def run(csa_1st_str=nil) while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout))) begin $mutex.lock @@ -220,7 +423,7 @@ class Player end if (@write_queue.size > Max_Write_Queue_Size) log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size)) - return + return end if (@status == "finished") @@ -228,15 +431,27 @@ class Player end str.chomp! if (str.class == String) case str - when /^[\+\-%][^%]/, :timeout + when /^[\+\-][^%]/ + if (@status == "game") + array_str = str.split(",") + move = array_str.shift + additional = array_str.shift + if /^'(.*)/ =~ additional + comment = array_str.unshift("'*#{$1}") + end + s = @game.handle_one_move(move, self) + @game.fh.print("#{comment}\n") if (comment && !s) + return if (s && @protocol == LoginCSA::PROTOCOL) + end + when /^%[^%]/, :timeout if (@status == "game") s = @game.handle_one_move(str, self) - return if (s && @protocol == "CSA") + return if (s && @protocol == LoginCSA::PROTOCOL) end when /^REJECT/ if (@status == "agree_waiting") @game.reject(@name) - return if (@protocol == "CSA") + 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 @@ -272,14 +487,28 @@ class Player end when /^%%HELP/ write_help + 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_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*$/ - @status = "connected" - @game_name = "" + 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 (! good_game_name?(game_name)) + if (! Login::good_game_name?(game_name)) write_safe(sprintf("##[ERROR] bad game name\n")) next elsif ((@status == "connected") || (@status == "game_waiting")) @@ -353,9 +582,8 @@ class Player 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) + if (player.protocol != LoginCSA::PROTOCOL) + player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) end end when /^%%LIST/ @@ -771,8 +999,10 @@ class Board @sente_history = Hash::new @gote_history = Hash::new @array = [[], [], [], [], [], [], [], [], [], []] + @move_count = 0 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) @@ -853,6 +1083,7 @@ class Board end @array[x0][y0].move_to(x1, y1) end + @move_count += 1 return true end @@ -979,11 +1210,21 @@ class Board end def good_kachi?(sente) - return false if (checkmated?(sente)) + if (checkmated?(sente)) + puts "'NG: Checkmating." if $DEBUG + return false + end + ou = look_for_ou(sente) - return false if (sente && (ou.y >= 4)) - return false if (! sente && (ou.y <= 6)) - + 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 @@ -1010,16 +1251,27 @@ class Board point = point + piece.point end - return false if (number < 10) + if (number < 10) + puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG + return false + end if (sente) - return false if (point < 28) + if (point < 28) + puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG + return false + end else - return false if (point < 27) + 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 - def handle_one_move(str) + def handle_one_move(str, sente=nil) if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/) sg = $1 x0 = $2.to_i @@ -1028,11 +1280,7 @@ class Board y1 = $5.to_i name = $6 elsif (str =~ /^%KACHI/) - if (@sente == @current_player) - sente = true - else - sente = false - end + raise ArgumentError, "sente is null", caller if sente == nil if (good_kachi?(sente)) return :kachi_win else @@ -1146,7 +1394,46 @@ class Board end end +class GameResult + attr_reader :players, :black, :white + + def initialize(p1, p2) + @players = [] + @players << p1 + @players << 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 + end + + def to_s + black_name = @black.id || @black.name + white_name = @white.id || @white.name + "%s:%s" % [black_name, white_name] + end +end + +class GameResultDraw < GameResult + +end + class Game + @@mutex = Mutex.new + @@time = 0 + def initialize(game_name, player0, player1) @monitors = Array::new @game_name = game_name @@ -1173,25 +1460,30 @@ class Game @sente.status = "agree_waiting" @gote.status = "agree_waiting" + @id = sprintf("%s+%s+%s+%s+%s", - LEAGUE.event, @game_name, @sente.name, @gote.name, - Time::new.strftime("%Y%m%d%H%M%S")) + LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time) + @logfile = @id + ".csa" LEAGUE.games[@id] = self - log_message(sprintf("game created %s", @id)) - @logfile = @id + ".csa" @board = Board::new @board.initial @start_time = nil @fh = nil + @result = nil propose end attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :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 monitoron(monitor) @monitors.delete(monitor) @@ -1227,10 +1519,10 @@ class Game @sente.status = "connected" @gote.status = "connected" - if (@current_player.protocol == "CSA") + if (@current_player.protocol == LoginCSA::PROTOCOL) @current_player.finish end - if (@next_player.protocol == "CSA") + if (@next_player.protocol == LoginCSA::PROTOCOL) @next_player.finish end @monitors = Array::new @@ -1245,21 +1537,27 @@ class Game finish_flag = true if (@current_player == player) @end_time = Time::new - t = (@end_time - @start_time).ceil + t = (@end_time - @start_time).floor t = Least_Time_Per_Move if (t < Least_Time_Per_Move) move_status = nil - if ((@current_player.mytime - t <= 0) && (@total_time > 0)) + 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 = @current_player.mytime - t + if (@current_player.mytime < 0) + @current_player.mytime = 0 + end + # begin - move_status = @board.handle_one_move(str) + move_status = @board.handle_one_move(str, @sente == @current_player) # rescue # log_error("handle_one_move raise exception for #{str}") # move_status = :illegal # end + if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore)) @fh.printf("'ILLEGAL_MOVE(%s)\n", str) else @@ -1270,6 +1568,7 @@ class Game @last_move = sprintf("%s,T%d", str, t) @current_turn = @current_turn + 1 end + @monitors.each do |monitor| monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] ")) monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id)) @@ -1277,12 +1576,6 @@ class Game end end - if (@current_player.mytime - t < @byoyomi) - @current_player.mytime = @byoyomi - else - @current_player.mytime = @current_player.mytime - t - end - if (@next_player.status != "game") # rival is logout or disconnected abnormal_win() elsif (status == :timeout) @@ -1323,6 +1616,8 @@ class Game @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", @id)) end @@ -1336,6 +1631,8 @@ class Game @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", @id)) end @@ -1348,6 +1645,8 @@ class Game @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", @id)) end @@ -1360,6 +1659,8 @@ class Game @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:oute_sennichite:%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] #OUTE_SENNICHITE\n", @id)) end @@ -1372,6 +1673,8 @@ class Game @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", @id)) end @@ -1384,6 +1687,8 @@ class Game @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", @id)) end @@ -1396,6 +1701,8 @@ class Game @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", @id)) end @@ -1408,6 +1715,8 @@ class Game @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", @id)) end @@ -1421,6 +1730,8 @@ class Game @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", @id)) end @@ -1434,6 +1745,8 @@ class Game @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", @id)) end @@ -1447,6 +1760,8 @@ class Game @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", @id)) end @@ -1459,6 +1774,8 @@ class Game @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", @id)) end @@ -1505,9 +1822,10 @@ EOM def show() str0 = <= 4) - client.close + login.incorrect_duplicated_player(str) Thread::exit + return end end LEAGUE.add(player) @@ -1744,15 +2053,17 @@ def main if (! player) client.close Thread::exit + return end log_message(sprintf("user %s login", player.name)) - player.run + login.process + player.run(login.csa_1st_str) begin $mutex.lock if (player.game) player.game.kill(player) end - player.finish + player.finish # socket has been closed LEAGUE.delete(player) log_message(sprintf("user %s logout", player.name)) ensure @@ -1761,8 +2072,16 @@ def main end end end +module_function :main + +end # module ShogiServer if ($0 == __FILE__) - LEAGUE = League::new - main + STDOUT.sync = true + STDERR.sync = true + TCPSocket.do_not_reverse_lookup = true + Thread.abort_on_exception = true + + LEAGUE = ShogiServer::League::new + ShogiServer::main end