4 # Author:: Daigo Moriwaki
5 # Homepage:: http://sourceforge.jp/projects/shogi-server/
8 # Copyright (C) 2013 Daigo Moriwaki (daigo at debian dot org)
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27 $:.unshift(File.join(File.dirname(File.expand_path(__FILE__)), ".."))
28 require 'shogi_server'
35 $logger = nil # main log IO
36 $engine = nil # engine IO
37 $server = nil # shogi server IO
43 #{File.basename($0)} - Brige program for a USI engine to connect to a CSA shogi server
46 #{File.basename($0)} [OPTIONS]... path_to_usi_engine
49 Bridge program for a USI engine to connect to a CSA shogi server
57 a host name to connect to a CSA server
59 player id for a CSA server
61 Interval in seconds to send a keep-alive packet to the server. [default 0]
64 directory to put log files
66 margin time [milliseconds] for byoyomi
68 option key and value for a USI engine. Use dedicated options
69 for USI_Ponder and USI_Hash.
70 ex --options "key_a=value_a,key_b=value_b"
72 password for a CSA server
76 a port number to connect to a CSA server. 4081 is often used.
81 GPL versoin 2 or later
86 #{ShogiServer::Revision}
91 # Parse command line options. Return a hash containing the option strings
92 # where a key is the option name without the first two slashes. For example,
93 # {"pid-file" => "foo.pid"}.
95 def parse_command_line
97 parser = GetoptLong.new(
98 ["--gamename", GetoptLong::REQUIRED_ARGUMENT],
99 ["--hash", GetoptLong::REQUIRED_ARGUMENT],
100 ["--host", GetoptLong::REQUIRED_ARGUMENT],
101 ["--id", GetoptLong::REQUIRED_ARGUMENT],
102 ["--keep-alive", GetoptLong::REQUIRED_ARGUMENT],
103 ["--log-dir", GetoptLong::REQUIRED_ARGUMENT],
104 ["--margin-msec", GetoptLong::REQUIRED_ARGUMENT],
105 ["--options", GetoptLong::REQUIRED_ARGUMENT],
106 ["--password", GetoptLong::REQUIRED_ARGUMENT],
107 ["--ponder", GetoptLong::NO_ARGUMENT],
108 ["--port", GetoptLong::REQUIRED_ARGUMENT])
111 parser.each_option do |name, arg|
114 options[name.to_sym] = arg.dup
118 raise parser.error_message
122 options[:gamename] ||= ENV["GAMENAME"] || "floodgate-900-0"
123 options[:hash] ||= ENV["HASH"] || 256
124 options[:hash] = options[:hash].to_i
125 options[:host] ||= ENV["HOST"] || "wdoor.c.u-tokyo.ac.jp"
126 options[:margin_msec] ||= ENV["MARGIN_MSEC"] || 2500
127 options[:id] ||= ENV["ID"]
128 options[:keep_alive] ||= ENV["KEEP_ALIVE"] || 0
129 options[:keep_alive] = options[:keep_alive].to_i
130 options[:log_dir] ||= ENV["LOG_DIR"] || "."
131 options[:password] ||= ENV["PASSWORD"]
132 options[:ponder] ||= ENV["PONDER"] || false
133 options[:port] ||= ENV["PORT"] || 4081
134 options[:port] = options[:port].to_i
139 # Check command line options.
140 # If any of them is invalid, exit the process.
142 def check_command_line
148 $options[:engine_path] = ARGV.shift
151 class BridgeFormatter < ::Logger::Formatter
154 @datetime_format = "%Y-%m-%dT%H:%M:%S.%6N"
157 def call(severity, time, progname, msg)
160 %!%s [%s]\n%s\n\n! % [format_datetime(time), severity, str]
164 def setup_logger(log_file)
165 logger = ShogiServer::Logger.new(log_file, 'daily')
166 logger.formatter = BridgeFormatter.new
167 logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
171 def log_engine_recv(msg)
172 $logger.info ">>> RECV LOG_ENGINE\n#{msg.gsub(/^/," ")}"
175 def log_engine_send(msg)
176 $logger.info "<<< SEND LOG_ENGINE\n#{msg.gsub(/^/," ")}"
179 def log_server_recv(msg)
180 $logger.info ">>> RECV LOG_SERVER\n#{msg.gsub(/^/," ")}"
183 def log_server_send(msg)
184 $logger.info "<<< SEND LOG_SERVER\n#{msg.gsub(/^/," ")}"
187 def log_info(msg, sout=true)
188 $stdout.puts msg if sout
197 # Holds the state of this Bridge program
202 %W!CONNECTED GAME_WAITING_CSA AGREE_WAITING_CSA GAME_CSA GAME_END PONDERING!.each do |s|
203 class_eval <<-EVAL, __FILE__, __LINE__ + 1
205 return @state == :#{s}
210 throw "Illegal state: #{@state}"
217 @state = :GAME_WAITING_CSA
218 @csaToUsi = ShogiServer::Usi::CsaToUsi.new
219 @usiToCsa = ShogiServer::Usi::UsiToCsa.new
220 @last_server_send_time = Time.now
223 @side = nil # my side; true for Black, false for White
224 @black_time = nil # milliseconds
225 @white_time = nil # milliseconds
226 @byoyomi = nil # milliseconds
241 def update_last_server_send_time
242 @last_server_send_time = Time.now
246 if $options[:keep_alive] <= 0
250 return $options[:keep_alive] < (Time.now - @last_server_send_time)
258 if (@byoyomi - $options[:margin_msec]) > 0
259 return (@byoyomi - $options[:margin_msec])
266 case $bridge_state.state
268 when :GAME_WAITING_CSA
270 when :AGREE_WAITING_CSA
272 when :GAME_CSA, :PONDERING
279 case $bridge_state.state
281 when :GAME_WAITING_CSA
282 when :AGREE_WAITING_CSA
283 when :GAME_CSA, :PONDERING
289 def parse_game_summary(str)
290 str.each_line do |line|
292 when /^Your_Turn:([\+\-])/
299 when /^Total_Time:(\d+)/
300 @black_time = $1.to_i * 1000
301 @white_time = $1.to_i * 1000
302 when /^Byoyomi:(\d+)/
303 @byoyomi = $1.to_i * 1000
307 if [@side, @black_time, @white_time, @byoyomi].include?(nil)
308 throw "Bad game summary: str"
312 def event_game_summary
313 assert_GAME_WAITING_CSA
315 str = recv_until($server, /^END Game_Summary/)
318 parse_game_summary(str)
321 transite :AGREE_WAITING_CSA
325 assert_AGREE_WAITING_CSA
328 return if str.nil? || str.strip.empty?
334 log_info "game crated #@game_id"
337 engine_puts "usinewgame"
339 engine_puts "position startpos"
340 engine_puts "go btime #@black_time wtime #@white_time byoyomi #{byoyomi()}"
344 log_info "game rejected."
347 throw "Bad message in #{@state}: #{str}"
351 def handle_one_move(usi)
352 state, csa = @usiToCsa.next(usi)
355 log_error "Found bad move #{usi} (#{csa}): #{state}"
364 def event_engine_recv
365 unless [:GAME_CSA, :PONDERING].include?(@state)
366 throw "Bad state at event_engine_recv: #@state"
370 return if str.nil? || str.strip.empty?
374 when /^bestmove\s+resign/
376 when /^bestmove\swin/
378 when /^bestmove\s+(.*)/
382 log_info "Ignore bestmove after 'stop'", false
383 # Trigger the next turn
386 engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time byoyomi #{byoyomi()}"
389 when /^(.*)\s+ponder\s+(.*)/
391 @ponder_move = $2.strip
396 moves = @usiToCsa.usi_moves.clone
397 moves << @ponder_move
398 engine_puts "position startpos moves #{moves.join(" ")}\ngo ponder btime #@black_time wtime #@white_time byoyomi #{byoyomi()}"
407 if /depth\s(\d+)/ =~ str
410 if /score\s+cp\s+(\d+)/ =~ str
416 if /pv\s+(.*)$/ =~str
422 def event_server_recv
423 unless [:GAME_CSA, :PONDERING].include?(@state)
424 throw "Bad state at event_engine_recv: #@state"
428 return if str.nil? || str.strip.empty?
432 when /^%TORYO,T(\d+)/
437 if %w!WIN LOSE DRAW!.include?(s)
439 engine_puts "gameover #{s.downcase}"
442 when /^([\+\-]\d{4}\w{2}),T(\d+)/
444 msec = $2.to_i * 1000
447 @black_time = [@black_time - msec, 0].max
449 @white_time = [@white_time - msec, 0].max
452 state1, usi = @csaToUsi.next(csa)
456 if csa[0..0] != (@side ? "+" : "-")
457 # Recive a new move from the opponent
458 state2, dummy = @usiToCsa.next(usi)
461 if usi == @ponder_move
462 engine_puts "ponderhit"
465 # Engine keeps on thinking
472 engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time byoyomi #{byoyomi()}"
479 if [@depth, @cp, @pv].include?(nil)
483 usiToCsa = @usiToCsa.deep_copy
485 if usiToCsa.usi_moves.last == pvs.first
492 state, csa = usiToCsa.next(usi)
502 return "'* #@cp #{moves.join(" ")}"
505 end # class BridgeState
507 def recv_until(io, regexp)
512 break if regexp =~ line
514 return lines.join("")
525 $bridge_state.update_last_server_send_time
528 # Start an engine process
531 log_info("Starting engine... #{$options[:engine_path]}")
533 cmd = %Q!| #{$options[:engine_path]}!
534 $engine = open(cmd, "w+")
537 select(nil, [$engine], nil)
538 log_engine_send "usi"
540 r = recv_until $engine, /usiok/
543 lines = ["setoption name USI_Hash value #{$options[:hash]}"]
544 lines << ["setoption name Hash value #{$options[:hash]}"] # for gpsfish
546 lines << "setoption name USI_Ponder value true"
547 lines << "setoption name Ponder value true" # for gpsfish
549 if $options[:options]
550 $options[:options].split(",").each do |str|
551 key, value = str.split("=")
552 lines << "setoption name #{key} value #{value}"
555 engine_puts lines.join("\n")
557 log_engine_send "isready"
558 $engine.puts "isready"
559 r = recv_until $engine, /readyok/
563 # Login to the shogi server
566 log_info("Connecting to #{$options[:host]}:#{$options[:port]}...")
568 $server = TCPSocket.open($options[:host], $options[:port])
571 log_error "Failed to connect to the server"
577 log_info("Login... #{$options[:gamename]} #{$options[:id]},xxxxxxxx")
578 if select(nil, [$server], nil, 15)
579 $server.puts "LOGIN #{$options[:id]} #{$options[:gamename]},#{$options[:password]}"
581 log_error("Failed to send login message to the server")
587 if select([$server], nil, nil, 15)
589 if /LOGIN:.* OK/ =~ line
592 log_error("Failed to login to the server")
598 log_error("Login attempt to the server timed out")
602 rescue Exception => ex
603 log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
614 ret, = select([$server, $engine], nil, nil, 60)
617 if $bridge_state.too_quiet?
619 $bridge_state.update_last_server_send_time
627 $bridge_state.do_engine_recv
629 $bridge_state.do_sever_recv
633 if $bridge_state.GAME_END?
634 log_info "game finished."
638 rescue Exception => ex
639 log_error "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace.join("\n\t")}"
645 $logger = setup_logger("main.log")
647 # Parse command line options
648 $options = parse_command_line
654 # Login to the shogi server
656 $bridge_state = BridgeState.new
657 log_info("Wait for a game start...")
667 TCPSocket.do_not_reverse_lookup = true
668 Thread.abort_on_exception = $DEBUG ? true : false
672 rescue Exception => ex
674 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
676 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"