• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
Aucun tag

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

shogi-server source


Commit MetaInfo

Révision22e18c529e0528a164004445d2430fbea4018cb5 (tree)
l'heure2013-12-15 22:35:39
AuteurDaigo Moriwaki <daigo@debi...>
CommiterDaigo Moriwaki

Message de Log

* [usiToCsa] - Added a new program, which is a bridge for a USI engine to connect to the Shogi-server.

Change Summary

Modification

--- /dev/null
+++ b/bin/usiToCsa
@@ -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
--- /dev/null
+++ b/bin/usiToCsa.rb
@@ -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
--- a/changelog
+++ b/changelog
@@ -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+
18 2013-12-13 Daigo Moriwaki <daigo at debian dot org>
29
310 * [shogi-server]
--- a/shogi_server/usi.rb
+++ b/shogi_server/usi.rb
@@ -140,7 +140,7 @@ module ShogiServer # for a namespace
140140 # Convert USI moves to CSA one by one from the initial position
141141 #
142142 class UsiToCsa
143- attr_reader :board, :csa_moves
143+ attr_reader :board, :csa_moves, :usi_moves
144144
145145 # Constructor
146146 #
@@ -149,24 +149,31 @@ module ShogiServer # for a namespace
149149 @board.initial
150150 @sente = true
151151 @csa_moves = []
152+ @usi_moves = []
153+ end
154+
155+ def deep_copy
156+ return Marshal.load(Marshal.dump(self))
152157 end
153158
154159 # Parses a usi move string and returns an array of [move_result_state,
155160 # csa_move_string]
156161 #
157162 def next(usi)
163+ usi_moves << usi
158164 csa = Usi.usiToCsa(usi, @board, @sente)
159165 state = @board.handle_one_move(csa, @sente)
160166 @sente = !@sente
161167 @csa_moves << csa
162168 return [state, csa]
163169 end
170+
164171 end # class UsiToCsa
165172
166173 # Convert CSA moves to USI one by one from the initial position
167174 #
168175 class CsaToUsi
169- attr_reader :board, :usi_moves
176+ attr_reader :board, :csa_moves, :usi_moves
170177
171178 # Constructor
172179 #
@@ -174,13 +181,19 @@ module ShogiServer # for a namespace
174181 @board = ShogiServer::Board.new
175182 @board.initial
176183 @sente = true
184+ @csa_moves = []
177185 @usi_moves = []
178186 end
179187
188+ def deep_copy
189+ return Marshal.load(Marshal.dump(self))
190+ end
191+
180192 # Parses a csa move string and returns an array of [move_result_state,
181193 # usi_move_string]
182194 #
183195 def next(csa)
196+ csa_moves << csa
184197 state = @board.handle_one_move(csa, @sente)
185198 @sente = !@sente
186199 usi = Usi.moveToUsi(@board.move)