## 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 'webrick'
require 'fileutils'
-
-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 Exception => ex
- log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
- return :exception
- 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
+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_Write_Queue_Size = 1000
Max_Identifier_Length = 32
Default_Timeout = 60 # for single socket operation
class League
- class Pairing
- def match(players)
- #
- end
-
- def start_game(p1, p2)
- p1.sente = true
- p2.sente = false
- Game.new(p1.game_name, p1, p2)
- end
-
- def delete_most_playing_player(players)
- if players.size % 2 == 1
- max_player = players.max {|a,b| a.win + a.loss <=> b.win + b.loss}
- players.delete(max_player)
- end
- end
- end # Pairing
-
- class RandomPairing < Pairing
- def match(players)
- if players.size < 2
- log_message("Floodgate: too few players [%d]" % [players.size])
- return
- end
- log_message("Floodgate: found %d players. Making games..." % [players.size])
- delete_most_playing_player(players)
-
- random_players = players.sort{ rand < 0.5 ? 1 : -1 }
- pairs = [[random_players.shift]]
- while !random_players.empty? do
- if pairs.last.size < 2
- pairs.last << random_players.shift
- else
- pairs << [random_players.shift]
- end
- end
- pairs.each do |pair|
- start_game(pair.first, pair.last)
- end
- end
- end # RadomPairing
-
class Floodgate
class << self
def game_name?(str)
def initialize(league)
@league = league
@next_time = nil
- @pairing = RandomPairing.new
charge
end
Floodgate.game_name?(pl.game_name) &&
pl.sente == nil
end
- @pairing.match(players)
+ 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
attr_accessor :players, :games, :event, :dir
def shutdown
+ @mutex.synchronize do
+ @players.each {|a| save(a)}
+ end
@floodgate.shutdown
end
def delete(player)
@mutex.synchronize do
+ save(player)
@players.delete(player.name)
end
end
def reload
- @players.each {|player| load(player)}
+ @mutex.synchronize do
+ @players.each {|player| load(player)}
+ end
end
def find_all_players
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
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']
- player.win = hash['win']
- player.loss = hash['loss']
+ 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.id]
+ if hash
+ hash['last_game_win'] = player.last_game_win
+ break
+ end
+ end
end
end
def search(id)
hash = nil
- @db.transaction do
+ @db.transaction(true) do
break unless @db["players"]
@db["players"].each do |group, players|
hash = players[id]
class Login
def Login.good_login?(str)
tokens = str.split
- if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
+ if (((tokens.length == 3) ||
+ ((tokens.length == 4) && tokens[3] == "x1")) &&
(tokens[0] == "LOGIN") &&
(good_identifier?(tokens[1])))
return true
def Login.factory(str, player)
(login, player.name, password, ext) = str.chomp.split
- if (ext)
+ if ext
return Loginx1.new(player, password)
else
return LoginCSA.new(player, password)
class BasicPlayer
+ def initialize
+ @id = nil
+ @name = nil
+ @password = nil
+ @last_game_win = false
+ end
+
# Idetifier of the player in the rating system
attr_accessor :id
# Last timestamp when the rate was modified
attr_accessor :modified_at
- def initialize
- @name = nil
- @password = nil
- end
+ # Whether win the previous game or not
+ attr_accessor :last_game_win
def modified_at
@modified_at || Time.now
@id != nil
end
+ def last_game_win?
+ return @last_game_win
+ end
+
def simple_id
if @trip
simple_name = @name.gsub(/@.*?$/, '')
class Player < BasicPlayer
- def initialize(str, socket)
+ def initialize(str, socket, eol=nil)
super()
@socket = socket
- @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
+ @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
@protocol = nil # CSA or x1
- @eol = "\m" # favorite eol code
+ @eol = eol || "\m" # favorite eol code
@game = nil
@game_name = ""
@mytime = 0 # set in start method also
@sente = nil
- @write_queue = Queue::new
+ @socket_buffer = []
@main_thread = Thread::current
- @writer_thread = Thread::start do
- Thread.pass
- writer()
- end
+ @mutex_write_guard = Mutex.new
end
attr_accessor :socket, :status
attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
- attr_accessor :main_thread, :writer_thread, :write_queue
+ attr_accessor :main_thread
+ attr_reader :socket_buffer
def kill
log_message(sprintf("user %s killed", @name))
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?)
rescue
end
def write_safe(str)
- @write_queue.push(str.gsub(/[\r\n]+/, @eol))
- end
-
- def writer
- while (str = @write_queue.pop)
+ @mutex_write_guard.synchronize do
begin
- @socket.write(str)
+ 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}.")
log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
- return
+ # TODO close
end
end
end
def to_s
- if ((status == "game_waiting") ||
- (status == "start_waiting") ||
- (status == "agree_waiting") ||
- (status == "game"))
+ 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)
end
def run(csa_1st_str=nil)
- while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
+ while (csa_1st_str ||
+ str = @socket_buffer.shift || gets_safe(@socket, Default_Timeout))
+ $mutex.lock
begin
- $mutex.lock
-
- if (@writer_thread == nil || @writer_thread.status == false)
- # The writer_thread has been killed because of socket errors.
- return
- end
+ log_message(str) if $DEBUG
if (csa_1st_str)
str = csa_1st_str
csa_1st_str = nil
end
- if (@write_queue.size > Max_Write_Queue_Size)
- log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
- return
- end
if (@status == "finished")
return
end
str.chomp! if (str.class == String) # may be strip! ?
- log_message(str) if $DEBUG
case str
when ""
# Application-level protocol for Keep-Alive
move = array_str.shift
additional = array_str.shift
if /^'(.*)/ =~ additional
- comment = array_str.unshift("'*#{$1}")
+ comment = array_str.unshift("'*#{$1.toeuc}")
end
s = @game.handle_one_move(move, self)
- @game.fh.print("#{comment}\n") if (comment && !s)
+ @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s)
return if (s && @protocol == LoginCSA::PROTOCOL)
end
when /^%[^%]/, :timeout
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)
+ 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
ensure
$mutex.unlock
end
+ unless @socket_buffer.empty?
+ sleep 10
+ 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"}
+ 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
@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
(1..9).each do |i|
PieceFU::new(self, i, 7, true)
end
+ @teban = true
end
def have_piece?(hands, name)
@array[x0][y0].move_to(x1, y1)
end
@move_count += 1
+ @teban = @teban ? false : true
return true
end
return false
end
else # gote
- if ((rival_ou.y != 0) &&
+ 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
return false
end
end
-
+
## case: rival_ou is moving
- escaped = false
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)
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)
- name = @array[x][y].promoted_name
+ names << @array[x][y].promoted_name
else
- name = @array[x][y].name
+ 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
- tmp_board = Marshal.load(Marshal.dump(self))
- s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
- raise "internal error" if (s != true)
- if (! tmp_board.checkmated?(! sente)) # good move
- return false
+ 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
return :illegal
end
-
if (sg == "+")
sente = true if sente == nil # deprecated
return :illegal unless sente == true # black player's move must be black
end
move_to(x0, y0, x1, y1, name, sente)
- str = to_s
update_sennichite(sente)
return :normal
end
a.push("\n")
end
- a.push("+\n")
+ a.push("%s\n" % [@teban ? "+" : "-"])
return a.join
end
end
def initialize(winner, loser)
super
@winner, @loser = winner, loser
+ @winner.last_game_win = true
+ @loser.last_game_win = false
end
def to_s
end
class GameResultDraw < GameResult
-
+ def initialize(p1, p2)
+ super
+ p1.last_game_win = false
+ p2.last_game_win = false
+ end
end
class Game
end
if (player0.sente)
- @sente = player0
- @gote = player1
+ @sente, @gote = player0, player1
else
- @sente = player1
- @gote = player0
+ @sente, @gote = player1, player0
end
- @current_player = @sente
- @next_player = @gote
-
+ @sente.socket_buffer.clear
+ @gote.socket_buffer.clear
+ @current_player, @next_player = @sente, @gote
@sente.game = self
- @gote.game = self
+ @gote.game = self
@last_move = ""
@current_turn = 0
@sente.status = "agree_waiting"
- @gote.status = "agree_waiting"
-
+ @gote.status = "agree_waiting"
+
@id = sprintf("%s+%s+%s+%s+%s",
- LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
+ LEAGUE.event, @game_name,
+ @sente.name, @gote.name, issue_current_time)
@logfile = File.join(LEAGUE.dir, @id + ".csa")
LEAGUE.games[@id] = self
end
def kill(killer)
- if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
+ if ["agree_waiting", "start_waiting"].include?(@sente.status)
reject(killer.name)
elsif (@current_player == killer)
abnormal_lose()
LEAGUE.games.delete(@id)
end
+ # class Game
def handle_one_move(str, player)
+ unless @current_player == player
+ @fh.puts("'Skipped %s" % [str])
+ log_warning("Skipped a move [%s] scince it is not %s 's turn." %
+ [str, player.name])
+ player.socket_buffer.unshift(str)
+ Thread.pass
+ return nil
+ end
+
finish_flag = true
- if (@current_player == player)
- @end_time = Time::new
- 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 <= -@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
+ @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
-# begin
- move_status = @board.handle_one_move(str, @sente == @current_player)
-# rescue
-# log_error("handle_one_move raise exception for #{str}")
-# move_status = :illegal
-# end
+ move_status = @board.handle_one_move(str, @sente == @current_player)
- if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
- @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
- else
- if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite_sente_lose) || (move_status == :oute_sennichite_gote_lose))
- @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 = @current_turn + 1
- end
+ 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][#{@id}] "))
- monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
- end
+ @monitors.each do |monitor|
+ monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
+ monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @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
+ 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
OPTIONS
--pid-file file
specify filename for logging process ID
- --daemon dir
- run as a daemon. Log files will be put in dir.
+ --daemon dir
+ run as a daemon. Log files will be put in dir.
LICENSE
this file is distributed under GPL version2 and might be compiled by Exerb
def parse_command_line
options = Hash::new
- parser = GetoptLong.new( ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
- ["--pid-file", GetoptLong::REQUIRED_ARGUMENT]
- )
+ parser = GetoptLong.new(
+ ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
+ ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
parser.quiet = true
begin
parser.each_option do |name, arg|
end
end
+def login_loop(client)
+ player = login = nil
+
+ while r = select([client], nil, nil, ShogiServer::Login_Time) do
+ break unless str = r[0].first.gets
+ $mutex.lock # guards LEAGUE
+ begin
+ str =~ /([\r\n]*)$/
+ eol = $1
+ if (ShogiServer::Login::good_login?(str))
+ player = ShogiServer::Player::new(str, client, eol)
+ login = ShogiServer::Login::factory(str, player)
+ if (current_player = LEAGUE.find(player.name))
+ if (current_player.password == player.password &&
+ current_player.status != "game")
+ log_message(sprintf("user %s login forcely", player.name))
+ current_player.kill
+ else
+ login.incorrect_duplicated_player(str)
+ player = nil
+ break
+ end
+ 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)
+ end
+ ensure
+ $mutex.unlock
+ end
+ end # login loop
+ return [player, login]
+end
+
def main
$mutex = Mutex::new
FileUtils.mkdir(dir)
end
log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
- $logger = WEBrick::Log.new(log_file)
+ $logger = WEBrick::Log.new(log_file) # thread safe
LEAGUE.dir = dir || File.dirname(__FILE__)
LEAGUE.setup_players_database
# 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 = nil
- login = nil
- while (str = client.gets_timeout(ShogiServer::Login_Time))
- begin
- $mutex.lock
- str =~ /([\r\n]*)$/
- eol = $1
- if (ShogiServer::Login::good_login?(str))
- player = ShogiServer::Player::new(str, client)
- player.eol = eol
- login = ShogiServer::Login::factory(str, player)
- if (LEAGUE.players[player.name])
- if ((LEAGUE.players[player.name].password == player.password) &&
- (LEAGUE.players[player.name].status != "game"))
- log_message(sprintf("user %s login forcely", player.name))
- LEAGUE.players[player.name].kill
- else
- login.incorrect_duplicated_player(str)
- #Thread::exit
- #return
- # TODO
- player = nil
- break
- end
- 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)
- end
- ensure
- $mutex.unlock
- end
- end # login loop
- if (! player)
- #client.close
- #Thread::exit
- #return
- next
- end
+ 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)
+ player.run(login.csa_1st_str) # loop
begin
$mutex.lock
if (player.game)
STDOUT.sync = true
STDERR.sync = true
TCPSocket.do_not_reverse_lookup = true
- Thread.abort_on_exception = true
+ Thread.abort_on_exception = $DEBUG ? true : false
LEAGUE = ShogiServer::League::new
main