shogi-server source
Révision | 22e18c529e0528a164004445d2430fbea4018cb5 (tree) |
---|---|
l'heure | 2013-12-15 22:35:39 |
Auteur | Daigo Moriwaki <daigo@debi...> |
Commiter | Daigo Moriwaki |
* [usiToCsa] - Added a new program, which is a bridge for a USI engine to connect to the Shogi-server.
@@ -0,0 +1,34 @@ | ||
1 | +#!/bin/sh | |
2 | + | |
3 | +engine=${1:?Specify engine binary path} | |
4 | +if [ ! -x "$engine" ] ; then | |
5 | + echo "Engine not found: $engine" | |
6 | + exit 1 | |
7 | +fi | |
8 | + | |
9 | +curdir=$(cd `dirname $0`; pwd) | |
10 | + | |
11 | +if [ -z "$ID" ] ; then | |
12 | + echo "Specify ID" | |
13 | + exit 1 | |
14 | +fi | |
15 | + | |
16 | +if [ -z "$PASSWORD" ] ; then | |
17 | + password_file="$HOME/.$ID.password" | |
18 | + if [ ! -f "$password_file" ] ; then | |
19 | + echo "Prepare a passowrd file at $password_file" | |
20 | + fi | |
21 | + export PASSWORD=`cat "$password_file"` | |
22 | +fi | |
23 | + | |
24 | +while true | |
25 | +do | |
26 | + logger -s "$ID: Restarting..." | |
27 | + | |
28 | + $curdir/usiToCsa.rb "$engine" | |
29 | + | |
30 | + if [ $? -ne 0 ] ; then | |
31 | + logger -s "$ID: Sleeping..." | |
32 | + sleep 900 | |
33 | + fi | |
34 | +done |
@@ -0,0 +1,681 @@ | ||
1 | +#!/usr/bin/env ruby | |
2 | +# $Id$ | |
3 | +# | |
4 | +# Author:: Daigo Moriwaki | |
5 | +# Homepage:: http://sourceforge.jp/projects/shogi-server/ | |
6 | +# | |
7 | +#-- | |
8 | +# Copyright (C) 2013 Daigo Moriwaki (daigo at debian dot org) | |
9 | +# | |
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. | |
14 | +# | |
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. | |
19 | +# | |
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 | |
23 | +#++ | |
24 | +# | |
25 | +# | |
26 | + | |
27 | +$:.unshift(File.join(File.dirname(File.expand_path(__FILE__)), "..")) | |
28 | +require 'shogi_server' | |
29 | +require 'logger' | |
30 | +require 'socket' | |
31 | + | |
32 | +# Global variables | |
33 | + | |
34 | +$options = nil | |
35 | +$logger = nil # main log IO | |
36 | +$engine = nil # engine IO | |
37 | +$server = nil # shogi server IO | |
38 | +$bridge_state = nil | |
39 | + | |
40 | +def usage | |
41 | + print <<EOM | |
42 | +NAME | |
43 | + #{File.basename($0)} - Brige program for a USI engine to connect to a CSA shogi server | |
44 | + | |
45 | +SYNOPSIS | |
46 | + #{File.basename($0)} [OPTIONS]... path_to_usi_engine | |
47 | + | |
48 | +DESCRIPTION | |
49 | + Bridge program for a USI engine to connect to a CSA shogi server | |
50 | + | |
51 | +OPTIONS | |
52 | + gamename | |
53 | + a gamename | |
54 | + hash | |
55 | + hash size in MB | |
56 | + host | |
57 | + a host name to connect to a CSA server | |
58 | + id | |
59 | + player id for a CSA server | |
60 | + keep-alive | |
61 | + Interval in seconds to send a keep-alive packet to the server. [default 0] | |
62 | + Disabled if it is 0. | |
63 | + log-dir | |
64 | + directory to put log files | |
65 | + margin-msec | |
66 | + margin time [milliseconds] for byoyomi | |
67 | + options | |
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" | |
71 | + password | |
72 | + password for a CSA server | |
73 | + ponder | |
74 | + enble ponder | |
75 | + port | |
76 | + a port number to connect to a CSA server. 4081 is often used. | |
77 | + | |
78 | +EXAMPLES | |
79 | + | |
80 | +LICENSE | |
81 | + GPL versoin 2 or later | |
82 | + | |
83 | +SEE ALSO | |
84 | + | |
85 | +REVISION | |
86 | + #{ShogiServer::Revision} | |
87 | + | |
88 | +EOM | |
89 | +end | |
90 | + | |
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"}. | |
94 | +# | |
95 | +def parse_command_line | |
96 | + options = Hash::new | |
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]) | |
109 | + parser.quiet = true | |
110 | + begin | |
111 | + parser.each_option do |name, arg| | |
112 | + name.sub!(/^--/, '') | |
113 | + name.sub!(/-/,'_') | |
114 | + options[name.to_sym] = arg.dup | |
115 | + end | |
116 | + rescue | |
117 | + usage | |
118 | + raise parser.error_message | |
119 | + end | |
120 | + | |
121 | + # Set default values | |
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 | |
135 | + | |
136 | + return options | |
137 | +end | |
138 | + | |
139 | +# Check command line options. | |
140 | +# If any of them is invalid, exit the process. | |
141 | +# | |
142 | +def check_command_line | |
143 | + if (ARGV.length < 1) | |
144 | + usage | |
145 | + exit 2 | |
146 | + end | |
147 | + | |
148 | + $options[:engine_path] = ARGV.shift | |
149 | +end | |
150 | + | |
151 | +class BridgeFormatter < ::Logger::Formatter | |
152 | + def initialize | |
153 | + super | |
154 | + @datetime_format = "%Y-%m-%dT%H:%M:%S.%6N" | |
155 | + end | |
156 | + | |
157 | + def call(severity, time, progname, msg) | |
158 | + str = msg2str(msg) | |
159 | + str.strip! if str | |
160 | + %!%s [%s]\n%s\n\n! % [format_datetime(time), severity, str] | |
161 | + end | |
162 | +end | |
163 | + | |
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 | |
168 | + return logger | |
169 | +end | |
170 | + | |
171 | +def log_engine_recv(msg) | |
172 | + $logger.info ">>> RECV LOG_ENGINE\n#{msg.gsub(/^/," ")}" | |
173 | +end | |
174 | + | |
175 | +def log_engine_send(msg) | |
176 | + $logger.info "<<< SEND LOG_ENGINE\n#{msg.gsub(/^/," ")}" | |
177 | +end | |
178 | + | |
179 | +def log_server_recv(msg) | |
180 | + $logger.info ">>> RECV LOG_SERVER\n#{msg.gsub(/^/," ")}" | |
181 | +end | |
182 | + | |
183 | +def log_server_send(msg) | |
184 | + $logger.info "<<< SEND LOG_SERVER\n#{msg.gsub(/^/," ")}" | |
185 | +end | |
186 | + | |
187 | +def log_info(msg, sout=true) | |
188 | + $stdout.puts msg if sout | |
189 | + $logger.info msg | |
190 | +end | |
191 | + | |
192 | +def log_error(msg) | |
193 | + $stdout.puts msg | |
194 | + $logger.error msg | |
195 | +end | |
196 | + | |
197 | +# Holds the state of this Bridge program | |
198 | +# | |
199 | +class BridgeState | |
200 | + attr_reader :state | |
201 | + | |
202 | + %W!CONNECTED GAME_WAITING_CSA AGREE_WAITING_CSA GAME_CSA GAME_END PONDERING!.each do |s| | |
203 | + class_eval <<-EVAL, __FILE__, __LINE__ + 1 | |
204 | + def #{s}? | |
205 | + return @state == :#{s} | |
206 | + end | |
207 | + | |
208 | + def assert_#{s} | |
209 | + unless #{s}? | |
210 | + throw "Illegal state: #{@state}" | |
211 | + end | |
212 | + end | |
213 | + EVAL | |
214 | + end | |
215 | + | |
216 | + def initialize | |
217 | + @state = :GAME_WAITING_CSA | |
218 | + @csaToUsi = ShogiServer::Usi::CsaToUsi.new | |
219 | + @usiToCsa = ShogiServer::Usi::UsiToCsa.new | |
220 | + @last_server_send_time = Time.now | |
221 | + | |
222 | + @game_id = nil | |
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 | |
227 | + | |
228 | + @depth = nil | |
229 | + @cp = nil | |
230 | + @pv = nil | |
231 | + @ponder_move = nil | |
232 | + end | |
233 | + | |
234 | + def next_turn | |
235 | + @depth = nil | |
236 | + @cp = nil | |
237 | + @pv = nil | |
238 | + @ponder_move = nil | |
239 | + end | |
240 | + | |
241 | + def update_last_server_send_time | |
242 | + @last_server_send_time = Time.now | |
243 | + end | |
244 | + | |
245 | + def too_quiet? | |
246 | + if $options[:keep_alive] <= 0 | |
247 | + return false | |
248 | + end | |
249 | + | |
250 | + return $options[:keep_alive] < (Time.now - @last_server_send_time) | |
251 | + end | |
252 | + | |
253 | + def transite(state) | |
254 | + @state = state | |
255 | + end | |
256 | + | |
257 | + def byoyomi | |
258 | + if (@byoyomi - $options[:margin_msec]) > 0 | |
259 | + return (@byoyomi - $options[:margin_msec]) | |
260 | + else | |
261 | + return @byoyomi | |
262 | + end | |
263 | + end | |
264 | + | |
265 | + def do_sever_recv | |
266 | + case $bridge_state.state | |
267 | + when :CONNECTED | |
268 | + when :GAME_WAITING_CSA | |
269 | + event_game_summary | |
270 | + when :AGREE_WAITING_CSA | |
271 | + event_game_start | |
272 | + when :GAME_CSA, :PONDERING | |
273 | + event_server_recv | |
274 | + when :GAME_END | |
275 | + end | |
276 | + end | |
277 | + | |
278 | + def do_engine_recv | |
279 | + case $bridge_state.state | |
280 | + when :CONNECTED | |
281 | + when :GAME_WAITING_CSA | |
282 | + when :AGREE_WAITING_CSA | |
283 | + when :GAME_CSA, :PONDERING | |
284 | + event_engine_recv | |
285 | + when :GAME_END | |
286 | + end | |
287 | + end | |
288 | + | |
289 | + def parse_game_summary(str) | |
290 | + str.each_line do |line| | |
291 | + case line.strip | |
292 | + when /^Your_Turn:([\+\-])/ | |
293 | + case $1 | |
294 | + when "+" | |
295 | + @side = true | |
296 | + when "-" | |
297 | + @side = false | |
298 | + end | |
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 | |
304 | + end | |
305 | + end | |
306 | + | |
307 | + if [@side, @black_time, @white_time, @byoyomi].include?(nil) | |
308 | + throw "Bad game summary: str" | |
309 | + end | |
310 | + end | |
311 | + | |
312 | + def event_game_summary | |
313 | + assert_GAME_WAITING_CSA | |
314 | + | |
315 | + str = recv_until($server, /^END Game_Summary/) | |
316 | + log_server_recv str | |
317 | + | |
318 | + parse_game_summary(str) | |
319 | + | |
320 | + server_puts "AGREE" | |
321 | + transite :AGREE_WAITING_CSA | |
322 | + end | |
323 | + | |
324 | + def event_game_start | |
325 | + assert_AGREE_WAITING_CSA | |
326 | + | |
327 | + str = $server.gets | |
328 | + return if str.nil? || str.strip.empty? | |
329 | + log_server_recv str | |
330 | + | |
331 | + case str | |
332 | + when /^START:(.*)/ | |
333 | + @game_id = $1 | |
334 | + log_info "game crated #@game_id" | |
335 | + | |
336 | + next_turn | |
337 | + engine_puts "usinewgame" | |
338 | + if @side | |
339 | + engine_puts "position startpos" | |
340 | + engine_puts "go btime #@black_time wtime #@white_time byoyomi #{byoyomi()}" | |
341 | + end | |
342 | + transite :GAME_CSA | |
343 | + when /^REJECT:(.*)/ | |
344 | + log_info "game rejected." | |
345 | + transite :GAME_END | |
346 | + else | |
347 | + throw "Bad message in #{@state}: #{str}" | |
348 | + end | |
349 | + end | |
350 | + | |
351 | + def handle_one_move(usi) | |
352 | + state, csa = @usiToCsa.next(usi) | |
353 | + # TODO state :normal | |
354 | + if state != :normal | |
355 | + log_error "Found bad move #{usi} (#{csa}): #{state}" | |
356 | + end | |
357 | + c = comment() | |
358 | + unless c.empty? | |
359 | + csa += ",#{c}" | |
360 | + end | |
361 | + server_puts csa | |
362 | + end | |
363 | + | |
364 | + def event_engine_recv | |
365 | + unless [:GAME_CSA, :PONDERING].include?(@state) | |
366 | + throw "Bad state at event_engine_recv: #@state" | |
367 | + end | |
368 | + | |
369 | + str = $engine.gets | |
370 | + return if str.nil? || str.strip.empty? | |
371 | + log_engine_recv str | |
372 | + | |
373 | + case str.strip | |
374 | + when /^bestmove\s+resign/ | |
375 | + server_puts "%TYORO" | |
376 | + when /^bestmove\swin/ | |
377 | + server_puts "%KACHI" | |
378 | + when /^bestmove\s+(.*)/ | |
379 | + str = $1.strip | |
380 | + | |
381 | + if PONDERING? | |
382 | + log_info "Ignore bestmove after 'stop'", false | |
383 | + # Trigger the next turn | |
384 | + transite :GAME_CSA | |
385 | + next_turn | |
386 | + engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time byoyomi #{byoyomi()}" | |
387 | + else | |
388 | + case str | |
389 | + when /^(.*)\s+ponder\s+(.*)/ | |
390 | + usi = $1.strip | |
391 | + @ponder_move = $2.strip | |
392 | + | |
393 | + handle_one_move(usi) | |
394 | + | |
395 | + if $options[:ponder] | |
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()}" | |
399 | + transite :PONDERING | |
400 | + end | |
401 | + else | |
402 | + handle_one_move(str) | |
403 | + end | |
404 | + end | |
405 | + when /^info\s+(.*)/ | |
406 | + str = $1 | |
407 | + if /depth\s(\d+)/ =~ str | |
408 | + @depth = $1 | |
409 | + end | |
410 | + if /score\s+cp\s+(\d+)/ =~ str | |
411 | + @cp = $1.to_i | |
412 | + if !@side | |
413 | + @cp *= -1 | |
414 | + end | |
415 | + end | |
416 | + if /pv\s+(.*)$/ =~str | |
417 | + @pv = $1 | |
418 | + end | |
419 | + end | |
420 | + end | |
421 | + | |
422 | + def event_server_recv | |
423 | + unless [:GAME_CSA, :PONDERING].include?(@state) | |
424 | + throw "Bad state at event_engine_recv: #@state" | |
425 | + end | |
426 | + | |
427 | + str = $server.gets | |
428 | + return if str.nil? || str.strip.empty? | |
429 | + log_server_recv str | |
430 | + | |
431 | + case str.strip | |
432 | + when /^%TORYO,T(\d+)/ | |
433 | + log_info str | |
434 | + when /^#(\w+)/ | |
435 | + s = $1 | |
436 | + log_info str | |
437 | + if %w!WIN LOSE DRAW!.include?(s) | |
438 | + server_puts "LOGOUT" | |
439 | + engine_puts "gameover #{s.downcase}" | |
440 | + transite :GAME_END | |
441 | + end | |
442 | + when /^([\+\-]\d{4}\w{2}),T(\d+)/ | |
443 | + csa = $1 | |
444 | + msec = $2.to_i * 1000 | |
445 | + | |
446 | + if csa[0..0] == "+" | |
447 | + @black_time = [@black_time - msec, 0].max | |
448 | + else | |
449 | + @white_time = [@white_time - msec, 0].max | |
450 | + end | |
451 | + | |
452 | + state1, usi = @csaToUsi.next(csa) | |
453 | + | |
454 | + # TODO state | |
455 | + | |
456 | + if csa[0..0] != (@side ? "+" : "-") | |
457 | + # Recive a new move from the opponent | |
458 | + state2, dummy = @usiToCsa.next(usi) | |
459 | + | |
460 | + if PONDERING? | |
461 | + if usi == @ponder_move | |
462 | + engine_puts "ponderhit" | |
463 | + transite :GAME_CSA | |
464 | + next_turn | |
465 | + # Engine keeps on thinking | |
466 | + else | |
467 | + engine_puts "stop" | |
468 | + end | |
469 | + else | |
470 | + transite :GAME_CSA | |
471 | + next_turn | |
472 | + engine_puts "position startpos moves #{@csaToUsi.usi_moves.join(" ")}\ngo btime #@black_time wtime #@white_time byoyomi #{byoyomi()}" | |
473 | + end | |
474 | + end | |
475 | + end | |
476 | + end | |
477 | + | |
478 | + def comment | |
479 | + if [@depth, @cp, @pv].include?(nil) | |
480 | + return "" | |
481 | + end | |
482 | + | |
483 | + usiToCsa = @usiToCsa.deep_copy | |
484 | + pvs = @pv.split(" ") | |
485 | + if usiToCsa.usi_moves.last == pvs.first | |
486 | + pvs.shift | |
487 | + end | |
488 | + | |
489 | + moves = [] | |
490 | + pvs.each do |usi| | |
491 | + begin | |
492 | + state, csa = usiToCsa.next(usi) | |
493 | + moves << csa | |
494 | + rescue | |
495 | + # ignore | |
496 | + end | |
497 | + end | |
498 | + | |
499 | + if moves.empty? | |
500 | + return "" | |
501 | + else | |
502 | + return "'* #@cp #{moves.join(" ")}" | |
503 | + end | |
504 | + end | |
505 | +end # class BridgeState | |
506 | + | |
507 | +def recv_until(io, regexp) | |
508 | + lines = [] | |
509 | + while line = io.gets | |
510 | + #puts "=== #{line}" | |
511 | + lines << line | |
512 | + break if regexp =~ line | |
513 | + end | |
514 | + return lines.join("") | |
515 | +end | |
516 | + | |
517 | +def engine_puts(str) | |
518 | + log_engine_send str | |
519 | + $engine.puts str | |
520 | +end | |
521 | + | |
522 | +def server_puts(str) | |
523 | + log_server_send str | |
524 | + $server.puts str | |
525 | +end | |
526 | + | |
527 | +# Start an engine process | |
528 | +# | |
529 | +def start_engine | |
530 | + log_info("Starting engine... #{$options[:engine_path]}") | |
531 | + | |
532 | + cmd = %Q!| #{$options[:engine_path]}! | |
533 | + $engine = open(cmd, "w+") | |
534 | + $engine.sync = true | |
535 | + | |
536 | + select(nil, [$engine], nil) | |
537 | + log_engine_send "usi" | |
538 | + $engine.puts "usi" | |
539 | + r = recv_until $engine, /usiok/ | |
540 | + log_engine_recv r | |
541 | + | |
542 | + lines = ["setoption name USI_Hash value #{$options[:hash]}"] | |
543 | + lines << ["setoption name Hash value #{$options[:hash]}"] # for gpsfish | |
544 | + if $options[:ponder] | |
545 | + lines << "setoption name USI_Ponder value true" | |
546 | + lines << "setoption name Ponder value true" # for gpsfish | |
547 | + end | |
548 | + if $options[:options] | |
549 | + $options[:options].split(",").each do |str| | |
550 | + key, value = str.split("=") | |
551 | + lines << "setoption name #{key} value #{value}" | |
552 | + end | |
553 | + end | |
554 | + engine_puts lines.join("\n") | |
555 | + | |
556 | + log_engine_send "isready" | |
557 | + $engine.puts "isready" | |
558 | + r = recv_until $engine, /readyok/ | |
559 | + log_engine_recv r | |
560 | +end | |
561 | + | |
562 | +# Login to the shogi server | |
563 | +# | |
564 | +def login | |
565 | + log_info("Connecting to #{$options[:host]}:#{$options[:port]}...") | |
566 | + begin | |
567 | + $server = TCPSocket.open($options[:host], $options[:port]) | |
568 | + $server.sync = true | |
569 | + rescue | |
570 | + log_error "Failed to connect to the server" | |
571 | + $server = nil | |
572 | + return false | |
573 | + end | |
574 | + | |
575 | + begin | |
576 | + log_info("Login... #{$options[:gamename]} #{$options[:id]},xxxxxxxx") | |
577 | + if select(nil, [$server], nil, 15) | |
578 | + $server.puts "LOGIN #{$options[:id]} #{$options[:gamename]},#{$options[:password]}" | |
579 | + else | |
580 | + log_error("Failed to send login message to the server") | |
581 | + $server.close | |
582 | + $server = nil | |
583 | + return false | |
584 | + end | |
585 | + | |
586 | + if select([$server], nil, nil, 15) | |
587 | + line = $server.gets | |
588 | + if /LOGIN:.* OK/ =~ line | |
589 | + log_info(line) | |
590 | + else | |
591 | + log_error("Failed to login to the server") | |
592 | + $server.close | |
593 | + $server = nil | |
594 | + return false | |
595 | + end | |
596 | + else | |
597 | + log_error("Login attempt to the server timed out") | |
598 | + $server.close | |
599 | + $server = nil | |
600 | + end | |
601 | + rescue Exception => ex | |
602 | + log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") | |
603 | + return false | |
604 | + end | |
605 | + | |
606 | + return true | |
607 | +end | |
608 | + | |
609 | +# MAIN LOOP | |
610 | +# | |
611 | +def main_loop | |
612 | + while true | |
613 | + ret, = select([$server, $engine], nil, nil, 60) | |
614 | + unless ret | |
615 | + # Send keep-alive | |
616 | + if @bridge_state.too_quiet? | |
617 | + $server.puts "" | |
618 | + @bridge_state.update_last_server_send_time | |
619 | + end | |
620 | + next | |
621 | + end | |
622 | + | |
623 | + ret.each do |io| | |
624 | + case io | |
625 | + when $engine | |
626 | + $bridge_state.do_engine_recv | |
627 | + when $server | |
628 | + $bridge_state.do_sever_recv | |
629 | + end | |
630 | + end | |
631 | + | |
632 | + if $bridge_state.GAME_END? | |
633 | + log_info "game finished." | |
634 | + break | |
635 | + end | |
636 | + end | |
637 | +rescue Exception => ex | |
638 | + log_error "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace.join("\n\t")}" | |
639 | +end | |
640 | + | |
641 | +# MAIN | |
642 | +# | |
643 | +def main | |
644 | + $logger = setup_logger("main.log") | |
645 | + | |
646 | + # Parse command line options | |
647 | + $options = parse_command_line | |
648 | + check_command_line | |
649 | + | |
650 | + # Start engine | |
651 | + start_engine | |
652 | + | |
653 | + # Login to the shogi server | |
654 | + if login | |
655 | + $bridge_state = BridgeState.new | |
656 | + log_info("Wait for a game start...") | |
657 | + main_loop | |
658 | + else | |
659 | + exit 1 | |
660 | + end | |
661 | +end | |
662 | + | |
663 | +if ($0 == __FILE__) | |
664 | + STDOUT.sync = true | |
665 | + STDERR.sync = true | |
666 | + TCPSocket.do_not_reverse_lookup = true | |
667 | + Thread.abort_on_exception = $DEBUG ? true : false | |
668 | + | |
669 | + begin | |
670 | + main | |
671 | + rescue Exception => ex | |
672 | + if $logger | |
673 | + log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") | |
674 | + else | |
675 | + $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" | |
676 | + end | |
677 | + exit 1 | |
678 | + end | |
679 | + | |
680 | + exit 0 | |
681 | +end |
@@ -1,3 +1,10 @@ | ||
1 | +2013-12-14 Daigo Moriwaki <daigo at debian dot org> | |
2 | + | |
3 | + * [usiToCsa] | |
4 | + - Added a new program, bin/usiToCsa.rb, which is a bridge for a | |
5 | + USI engine to connect to the Shogi-server. | |
6 | + - bin/usiToCsa is a sample wrapper script. | |
7 | + | |
1 | 8 | 2013-12-13 Daigo Moriwaki <daigo at debian dot org> |
2 | 9 | |
3 | 10 | * [shogi-server] |
@@ -140,7 +140,7 @@ module ShogiServer # for a namespace | ||
140 | 140 | # Convert USI moves to CSA one by one from the initial position |
141 | 141 | # |
142 | 142 | class UsiToCsa |
143 | - attr_reader :board, :csa_moves | |
143 | + attr_reader :board, :csa_moves, :usi_moves | |
144 | 144 | |
145 | 145 | # Constructor |
146 | 146 | # |
@@ -149,24 +149,31 @@ module ShogiServer # for a namespace | ||
149 | 149 | @board.initial |
150 | 150 | @sente = true |
151 | 151 | @csa_moves = [] |
152 | + @usi_moves = [] | |
153 | + end | |
154 | + | |
155 | + def deep_copy | |
156 | + return Marshal.load(Marshal.dump(self)) | |
152 | 157 | end |
153 | 158 | |
154 | 159 | # Parses a usi move string and returns an array of [move_result_state, |
155 | 160 | # csa_move_string] |
156 | 161 | # |
157 | 162 | def next(usi) |
163 | + usi_moves << usi | |
158 | 164 | csa = Usi.usiToCsa(usi, @board, @sente) |
159 | 165 | state = @board.handle_one_move(csa, @sente) |
160 | 166 | @sente = !@sente |
161 | 167 | @csa_moves << csa |
162 | 168 | return [state, csa] |
163 | 169 | end |
170 | + | |
164 | 171 | end # class UsiToCsa |
165 | 172 | |
166 | 173 | # Convert CSA moves to USI one by one from the initial position |
167 | 174 | # |
168 | 175 | class CsaToUsi |
169 | - attr_reader :board, :usi_moves | |
176 | + attr_reader :board, :csa_moves, :usi_moves | |
170 | 177 | |
171 | 178 | # Constructor |
172 | 179 | # |
@@ -174,13 +181,19 @@ module ShogiServer # for a namespace | ||
174 | 181 | @board = ShogiServer::Board.new |
175 | 182 | @board.initial |
176 | 183 | @sente = true |
184 | + @csa_moves = [] | |
177 | 185 | @usi_moves = [] |
178 | 186 | end |
179 | 187 | |
188 | + def deep_copy | |
189 | + return Marshal.load(Marshal.dump(self)) | |
190 | + end | |
191 | + | |
180 | 192 | # Parses a csa move string and returns an array of [move_result_state, |
181 | 193 | # usi_move_string] |
182 | 194 | # |
183 | 195 | def next(csa) |
196 | + csa_moves << csa | |
184 | 197 | state = @board.handle_one_move(csa, @sente) |
185 | 198 | @sente = !@sente |
186 | 199 | usi = Usi.moveToUsi(@board.move) |