• R/O
  • HTTP
  • SSH
  • HTTPS

タグ
未設定

よく使われているワード(クリックで追加)

javaandroidc++linuxc#windowsobjective-ccocoaqtpython誰得phprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

shogi-server source


ファイル情報

Rev. 6cc96f8c5ddab5d2f52b94ee857685646511fd16
サイズ 18,275 バイト
日時 2016-06-09 19:57:54
作者 dragon-fruit
ログメッセージ

・思考エンジン終了時に明示的にquitを送るように修正。

・終了時、サーバーと、エンジンを明示的にクローズするように修正

・Max_Movesで引き分けた時のCENSOREDコマンドへ対応

内容

#!/usr/bin/env ruby
# $Id$
#
# Author:: Daigo Moriwaki
# Homepage:: http://sourceforge.jp/projects/shogi-server/
#
#--
# Copyright (C) 2013 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
#++
#
#

$:.unshift(File.join(File.dirname(File.expand_path(__FILE__)), ".."))
require 'shogi_server'
require 'logger'
require 'socket'

# Global variables

$options = nil
$logger  = nil   # main log IO
$engine  = nil   # engine IO
$server  = nil   # shogi server IO
$bridge_state = nil

def usage
    print <<EOM
NAME
        #{File.basename($0)} - Brige program for a USI engine to connect to a CSA shogi server

SYNOPSIS
        #{File.basename($0)} [OPTIONS]... path_to_usi_engine

DESCRIPTION
        Bridge program for a USI engine to connect to a CSA shogi server

OPTIONS
        gamename
                a gamename
        hash
                hash size in MB
        host
                a host name to connect to a CSA server
        id
                player id for a CSA server
        keep-alive
                Interval in seconds to send a keep-alive packet to the server. [default 0]
                Disabled if it is 0.
        log-dir
                directory to put log files
        margin-msec
                margin time [milliseconds] for byoyomi
        options
                option key and value for a USI engine. Use dedicated options
                for USI_Ponder and USI_Hash.
                ex --options "key_a=value_a,key_b=value_b"
        password
                password for a CSA server
        ponder
                enble ponder
        port
                a port number to connect to a CSA server. 4081 is often used.

EXAMPLES

LICENSE
        GPL versoin 2 or later

SEE ALSO

REVISION
        #{ShogiServer::Revision}

EOM
end

# Parse command line options. Return a hash containing the option strings
# where a key is the option name without the first two slashes. For example,
# {"pid-file" => "foo.pid"}.
#
def parse_command_line
  options = Hash::new
  parser = GetoptLong.new(
    ["--gamename",    GetoptLong::REQUIRED_ARGUMENT],
    ["--hash",        GetoptLong::REQUIRED_ARGUMENT],
    ["--host",        GetoptLong::REQUIRED_ARGUMENT],
    ["--id",          GetoptLong::REQUIRED_ARGUMENT],
    ["--keep-alive",  GetoptLong::REQUIRED_ARGUMENT],
    ["--log-dir",     GetoptLong::REQUIRED_ARGUMENT],
    ["--margin-msec", GetoptLong::REQUIRED_ARGUMENT],
    ["--options",     GetoptLong::REQUIRED_ARGUMENT],
    ["--password",    GetoptLong::REQUIRED_ARGUMENT],
    ["--ponder",      GetoptLong::NO_ARGUMENT],
    ["--port",        GetoptLong::REQUIRED_ARGUMENT])
  parser.quiet = true
  begin
    parser.each_option do |name, arg|
      name.sub!(/^--/, '')
      name.sub!(/-/,'_')
      options[name.to_sym] = arg.dup
    end
  rescue
    usage
    raise parser.error_message
  end

  # Set default values
  options[:gamename]    ||= ENV["GAMENAME"] || "floodgate-900-0"
  options[:hash]        ||= ENV["HASH"] || 256
  options[:hash]        = options[:hash].to_i
  options[:host]        ||= ENV["HOST"] || "wdoor.c.u-tokyo.ac.jp"
  options[:margin_msec] ||= ENV["MARGIN_MSEC"] || 2500
  options[:margin_msec] = options[:margin_msec].to_i
  options[:id]          ||= ENV["ID"]
  options[:keep_alive]  ||= ENV["KEEP_ALIVE"] || 0
  options[:keep_alive]  = options[:keep_alive].to_i
  options[:log_dir]     ||= ENV["LOG_DIR"] || "."
  options[:password]    ||= ENV["PASSWORD"]
  options[:ponder]      ||= ENV["PONDER"] || false
  options[:port]        ||= ENV["PORT"] || 4081
  options[:port]        = options[:port].to_i

  return options
end

# Check command line options.
# If any of them is invalid, exit the process.
#
def check_command_line
  if (ARGV.length < 1)
    usage
    exit 2
  end

  $options[:engine_path] = ARGV.shift
end

class BridgeFormatter < ::Logger::Formatter
  def initialize
    super
    @datetime_format = "%Y-%m-%dT%H:%M:%S.%6N"
  end

  def call(severity, time, progname, msg)
    str = msg2str(msg)
    str.strip! if str
    %!%s [%s]\n%s\n\n! % [format_datetime(time), severity, str]
  end
end

def setup_logger(log_file)
  logger = ShogiServer::Logger.new(log_file, 'daily')
  logger.formatter = BridgeFormatter.new
  logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
  return logger
end

def log_engine_recv(msg)
  $logger.info ">>> RECV LOG_ENGINE\n#{msg.gsub(/^/,"    ")}"
end

def log_engine_send(msg)
  $logger.info "<<< SEND LOG_ENGINE\n#{msg.gsub(/^/,"    ")}"
end

def log_server_recv(msg)
  $logger.info ">>> RECV LOG_SERVER\n#{msg.gsub(/^/,"    ")}"
end

def log_server_send(msg)
  $logger.info "<<< SEND LOG_SERVER\n#{msg.gsub(/^/,"    ")}"
end

def log_info(msg, sout=true)
  $stdout.puts msg if sout
  $logger.info msg
end

def log_error(msg)
  $stdout.puts msg
  $logger.error msg
end

# Holds the state of this Bridge program
#
class BridgeState
  attr_reader :state

  %W!CONNECTED GAME_WAITING_CSA AGREE_WAITING_CSA GAME_CSA GAME_END PONDERING!.each do |s|
    class_eval <<-EVAL, __FILE__, __LINE__ + 1
      def #{s}?
        return @state == :#{s}
      end

      def assert_#{s}
        unless #{s}?
          throw "Illegal state: #{@state}"
        end
      end
    EVAL
  end

  def initialize
    @state      = :GAME_WAITING_CSA
    @csaToUsi   = ShogiServer::Usi::CsaToUsi.new
    @usiToCsa   = ShogiServer::Usi::UsiToCsa.new
    @last_server_send_time = Time.now

    @game_id    = nil
    @side       = nil    # my side; true for Black, false for White
    @black_time = nil    # milliseconds
    @white_time = nil    # milliseconds
    @byoyomi    = nil    # milliseconds
    @increment  = 0      # milliseconds (increment非対応のshogi-serverとの互換性のために初期値は0にしておく)

    @depth       = nil
    @cp          = nil
    @pv          = nil
    @ponder_move = nil
  end

  def next_turn
    @depth      = nil
    @cp         = nil
    @pv         = nil
    @ponder_move = nil
  end

  def update_last_server_send_time
    @last_server_send_time = Time.now
  end

  def too_quiet?
    if $options[:keep_alive] <= 0
      return false
    end

    return $options[:keep_alive] < (Time.now - @last_server_send_time)
  end

  def transite(state)
    @state   = state
  end

  def byoyomi
    if (@byoyomi - $options[:margin_msec]) > 0
      return (@byoyomi - $options[:margin_msec])
    else
      return @byoyomi
    end
  end

  def do_sever_recv
    case $bridge_state.state
    when :CONNECTED
    when :GAME_WAITING_CSA
      event_game_summary
    when :AGREE_WAITING_CSA
      event_game_start
    when :GAME_CSA, :PONDERING
      event_server_recv
    when :GAME_END
    end
  end

  def do_engine_recv
    case $bridge_state.state
    when :CONNECTED
    when :GAME_WAITING_CSA
    when :AGREE_WAITING_CSA
    when :GAME_CSA, :PONDERING
      event_engine_recv
    when :GAME_END
    end
  end

  def parse_game_summary(str)
    str.each_line do |line|
      case line.strip
      when /^Your_Turn:([\+\-])/
        case $1
        when "+"
          @side = true
        when "-"
          @side = false
        end
      when /^Total_Time:(\d+)/
        @black_time = $1.to_i * 1000
        @white_time = $1.to_i * 1000
      when /^Byoyomi:(\d+)/
        @byoyomi = $1.to_i * 1000
      when /^Increment:(\d+)/
        @increment = $1.to_i * 1000
      end
    end

    if [@side, @black_time, @white_time, @byoyomi].include?(nil)
      throw "Bad game summary: str"
    end
  end

  def event_game_summary
    assert_GAME_WAITING_CSA

    str = recv_until($server, /^END Game_Summary/)
    log_server_recv str

    parse_game_summary(str)

    server_puts "AGREE"
    transite :AGREE_WAITING_CSA
  end

  def event_game_start
    assert_AGREE_WAITING_CSA

    str = $server.gets
    return if str.nil? || str.strip.empty?
    log_server_recv str

    case str
    when /^START:(.*)/
      @game_id = $1
      @time_turn_start = Time.now
      log_info "game crated #@game_id"
      
      next_turn
      engine_puts "usinewgame"
      if @side
        engine_puts "position startpos"
        if @increment > 0 then
          engine_puts "go btime #@black_time wtime #@white_time binc #@increment winc #@increment"
        else
          engine_puts "go btime #@black_time wtime #@white_time byoyomi #{byoyomi()}"
        end
      end
      transite :GAME_CSA
    when /^REJECT:(.*)/
      log_info "game rejected."
      transite :GAME_END
    else         
      throw "Bad message in #{@state}: #{str}" 
    end
  end

  def handle_one_move(usi)
    state, csa  = @usiToCsa.next(usi)
    # TODO state :normal
    if state != :normal
      log_error "Found bad move #{usi} (#{csa}): #{state}"
    end
    c = comment()
    unless c.empty?
      csa += ",#{c}"
    end
    server_puts csa
  end

  def event_engine_recv
    unless [:GAME_CSA, :PONDERING].include?(@state)
      throw "Bad state at event_engine_recv: #@state"
    end

    str = $engine.gets
    return if str.nil? || str.strip.empty?
    log_engine_recv str

    case str.strip
    when /^bestmove\s+resign/
      server_puts "%TORYO"
    when /^bestmove\swin/
      server_puts "%KACHI"
    when /^bestmove\s+(.*)/
      str = $1.strip
      
      if PONDERING?
        log_info "Ignore bestmove after 'stop'", false
        # Trigger the next turn
        transite :GAME_CSA
        next_turn
        if @increment > 0 then
          engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time binc #@increment winc #@increment"
        else
          engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time byoyomi #{byoyomi()}"
        end
      else
        case str
        when /^(.*)\s+ponder\s+(.*)/
          usi          = $1.strip
          @ponder_move = $2.strip

          handle_one_move(usi)

          if $options[:ponder]
            moves = @usiToCsa.usi_moves.clone
            moves << @ponder_move
            btime_tmp = @black_time
            wtime_tmp = @white_time
            estimated_consumption = (Time.now - @time_turn_start).ceil * 1000 # give a some margin by ceiling the value
            if @side then
              btime_tmp = [btime_tmp + @increment - estimated_consumption, 0].max
            else
              wtime_tmp = [wtime_tmp + @increment - estimated_consumption, 0].max
            end
            if @increment > 0 then
              engine_puts "position startpos moves #{moves.join(" ")}\ngo ponder btime #{btime_tmp} wtime #{wtime_tmp} binc #@increment winc #@increment"
            else
              engine_puts "position startpos moves #{moves.join(" ")}\ngo ponder btime #{btime_tmp} wtime #{wtime_tmp} byoyomi #{byoyomi()}"
            end
            transite :PONDERING
          end
        else
          handle_one_move(str)
        end
      end
    when /^info\s+(.*)/
      str = $1
      if /(\s+|^)depth\s(\d+)/ =~ str
        @depth = $2
      end
      if /(\s+|^)score\s+cp\s+(-?\d+)/ =~ str
        @cp = $2.to_i
        if !@side
          @cp *= -1
        end
      elsif /(\s+|^)score\s+mate\s+(-?\d+)/ =~ str
        @cp = ($2.to_i < 0 ? -100000 : 100000)
        if !@side
          @cp *= -1
        end
      end
      if /(\s+|^)pv\s+(.*)$/ =~str
        @pv = $2
      end
    end
  end

  def event_server_recv
    unless [:GAME_CSA, :PONDERING].include?(@state)
      throw "Bad state at event_engine_recv: #@state"
    end

    str = $server.gets
    return if str.nil? || str.strip.empty?
    log_server_recv str

    case str.strip
    when /^%TORYO,T(\d+)/
      log_info str
    when /^#(\w+)/
      s = $1
      log_info str
      if %w!WIN LOSE DRAW CENSORED!.include?(s)
        server_puts "LOGOUT"
        engine_puts "gameover #{s.downcase}"
        transite :GAME_END
      end
    when /^([\+\-]\d{4}\w{2}),T(\d+)/
      csa  = $1
      msec = $2.to_i * 1000

      if csa[0..0] == "+"
        @black_time = [@black_time + @increment - msec, 0].max
        if !@side
          @time_turn_start = Time.now
        end
      else
        @white_time = [@white_time + @increment - msec, 0].max
        if @side
          @time_turn_start = Time.now
        end
      end

      state1, usi = @csaToUsi.next(csa)

      # TODO state
      
      if csa[0..0] != (@side ? "+" : "-")
        # Recive a new move from the opponent
        state2, dummy = @usiToCsa.next(usi)

        if PONDERING?
          if usi == @ponder_move
            engine_puts "ponderhit"
            transite :GAME_CSA
            #next_turn
            # Engine keeps on thinking
          else
            engine_puts "stop"
          end
        else
          transite :GAME_CSA
          next_turn
          if @increment > 0 then
            engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time binc #@increment winc #@increment"
          else
            engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time byoyomi #{byoyomi()}"
          end
        end
      end
    end
  end

  def comment
    if [@depth, @cp, @pv].include?(nil)
      return ""
    end

    usiToCsa = @usiToCsa.deep_copy
    pvs = @pv.split(" ")
    if usiToCsa.usi_moves.last == pvs.first
      pvs.shift
    end

    moves = []
    pvs.each do |usi|
      begin
        state, csa = usiToCsa.next(usi)
        moves << csa
      rescue
        # ignore
      end
    end
    
    if moves.empty?
      return ""
    else
      return "'* #@cp #{moves.join(" ")}"
    end
  end
end # class BridgeState

def recv_until(io, regexp)
  lines = []
  while line = io.gets
    #puts "=== #{line}"
    lines << line
    break if regexp =~ line
  end
  return lines.join("")
end

def engine_puts(str)
  log_engine_send str
  $engine.puts str
end

def server_puts(str)
  log_server_send str
  $server.puts str
  $bridge_state.update_last_server_send_time
end

# Start an engine process
#
def start_engine
  log_info("Starting engine...  #{$options[:engine_path]}")

  cmd = %Q!| #{$options[:engine_path]}!
  $engine = open(cmd, "w+")
  $engine.sync = true

  select(nil, [$engine], nil)
  log_engine_send "usi"
  $engine.puts "usi"
  r = recv_until $engine, /usiok/
  log_engine_recv r

  lines =  ["setoption name USI_Hash value #{$options[:hash]}"]
  lines << ["setoption name Hash value #{$options[:hash]}"] # for gpsfish
  if $options[:ponder]
    lines << "setoption name USI_Ponder value true"
    lines << "setoption name Ponder value true" # for gpsfish
  end
  if $options[:options] 
    $options[:options].split(",").each do |str|
      key, value = str.split("=")
      lines << "setoption name #{key} value #{value}"
    end
  end
  engine_puts lines.join("\n")

  log_engine_send "isready"
  $engine.puts "isready"
  r = recv_until $engine, /readyok/
  log_engine_recv r
end

# Login to the shogi server
#
def login
  log_info("Connecting to #{$options[:host]}:#{$options[:port]}...")
  begin
    $server = TCPSocket.open($options[:host], $options[:port])
    $server.sync = true
  rescue
    log_error "Failed to connect to the server"
    $server = nil
    return false
  end

  begin
    log_info("Login...  #{$options[:gamename]} #{$options[:id]},xxxxxxxx")
    if select(nil, [$server], nil, 15)
      $server.puts "LOGIN #{$options[:id]} #{$options[:gamename]},#{$options[:password]}"
    else
      log_error("Failed to send login message to the server")
      $server.close
      $server = nil
      return false
    end

    if select([$server], nil, nil, 15)
      line = $server.gets
      if /LOGIN:.* OK/ =~ line
        log_info(line)
      else
        log_error("Failed to login to the server")
        $server.close
        $server = nil
        return false
      end
    else
      log_error("Login attempt to the server timed out")
      $server.close
      $server = nil
    end
  rescue Exception => ex
    log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
    return false
  end

  return true
end

# MAIN LOOP
#
def main_loop
  while true
    ret, = select([$server, $engine], nil, nil, 60)
    unless ret
      # Send keep-alive
      if $bridge_state.too_quiet?
        $server.puts ""
        $bridge_state.update_last_server_send_time
      end
      next
    end

    ret.each do |io|
      case io
      when $engine
        $bridge_state.do_engine_recv
      when $server
        $bridge_state.do_sever_recv
      end
    end

    if $bridge_state.GAME_END?
      engine_puts "quit"
      log_info "game finished."
      break
    end
  end

  if $engine.nil?
    $engine.close
    $engile = nil
  end

  if $server.nil?
    $server.close
    $server = nil
  end

rescue Exception => ex
  log_error "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace.join("\n\t")}"
end

# MAIN
#
def main
  $logger = setup_logger("main.log")

  # Parse command line options
  $options = parse_command_line
  check_command_line

  # Start engine
  start_engine

  # Login to the shogi server
  if login
    $bridge_state = BridgeState.new
    log_info("Wait for a game start...")
    main_loop
  else
    exit 1
  end
end

if ($0 == __FILE__)
  STDOUT.sync = true
  STDERR.sync = true
  TCPSocket.do_not_reverse_lookup = true
  Thread.abort_on_exception = $DEBUG ? true : false

  begin
    main
  rescue Exception => ex
    if $logger
      log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
    else
      $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
    end
    exit 1
  end
  
  exit 0
end