• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

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

shogi-server source


Commit MetaInfo

Revisãocd9bfae56a0c922ac2c82895ad4765364b597738 (tree)
Hora2014-02-22 21:54:41
AutorDaigo Moriwaki <daigo@debi...>
CommiterDaigo Moriwaki

Mensagem de Log

Merge branch '201312-usiToCsa'

Conflicts:
changelog

Mudança Sumário

Diff

--- /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,682 @@
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 "%TORYO"
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+ $bridge_state.update_last_server_send_time
526+end
527+
528+# Start an engine process
529+#
530+def start_engine
531+ log_info("Starting engine... #{$options[:engine_path]}")
532+
533+ cmd = %Q!| #{$options[:engine_path]}!
534+ $engine = open(cmd, "w+")
535+ $engine.sync = true
536+
537+ select(nil, [$engine], nil)
538+ log_engine_send "usi"
539+ $engine.puts "usi"
540+ r = recv_until $engine, /usiok/
541+ log_engine_recv r
542+
543+ lines = ["setoption name USI_Hash value #{$options[:hash]}"]
544+ lines << ["setoption name Hash value #{$options[:hash]}"] # for gpsfish
545+ if $options[:ponder]
546+ lines << "setoption name USI_Ponder value true"
547+ lines << "setoption name Ponder value true" # for gpsfish
548+ end
549+ if $options[:options]
550+ $options[:options].split(",").each do |str|
551+ key, value = str.split("=")
552+ lines << "setoption name #{key} value #{value}"
553+ end
554+ end
555+ engine_puts lines.join("\n")
556+
557+ log_engine_send "isready"
558+ $engine.puts "isready"
559+ r = recv_until $engine, /readyok/
560+ log_engine_recv r
561+end
562+
563+# Login to the shogi server
564+#
565+def login
566+ log_info("Connecting to #{$options[:host]}:#{$options[:port]}...")
567+ begin
568+ $server = TCPSocket.open($options[:host], $options[:port])
569+ $server.sync = true
570+ rescue
571+ log_error "Failed to connect to the server"
572+ $server = nil
573+ return false
574+ end
575+
576+ begin
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]}"
580+ else
581+ log_error("Failed to send login message to the server")
582+ $server.close
583+ $server = nil
584+ return false
585+ end
586+
587+ if select([$server], nil, nil, 15)
588+ line = $server.gets
589+ if /LOGIN:.* OK/ =~ line
590+ log_info(line)
591+ else
592+ log_error("Failed to login to the server")
593+ $server.close
594+ $server = nil
595+ return false
596+ end
597+ else
598+ log_error("Login attempt to the server timed out")
599+ $server.close
600+ $server = nil
601+ end
602+ rescue Exception => ex
603+ log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
604+ return false
605+ end
606+
607+ return true
608+end
609+
610+# MAIN LOOP
611+#
612+def main_loop
613+ while true
614+ ret, = select([$server, $engine], nil, nil, 60)
615+ unless ret
616+ # Send keep-alive
617+ if $bridge_state.too_quiet?
618+ $server.puts ""
619+ $bridge_state.update_last_server_send_time
620+ end
621+ next
622+ end
623+
624+ ret.each do |io|
625+ case io
626+ when $engine
627+ $bridge_state.do_engine_recv
628+ when $server
629+ $bridge_state.do_sever_recv
630+ end
631+ end
632+
633+ if $bridge_state.GAME_END?
634+ log_info "game finished."
635+ break
636+ end
637+ end
638+rescue Exception => ex
639+ log_error "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace.join("\n\t")}"
640+end
641+
642+# MAIN
643+#
644+def main
645+ $logger = setup_logger("main.log")
646+
647+ # Parse command line options
648+ $options = parse_command_line
649+ check_command_line
650+
651+ # Start engine
652+ start_engine
653+
654+ # Login to the shogi server
655+ if login
656+ $bridge_state = BridgeState.new
657+ log_info("Wait for a game start...")
658+ main_loop
659+ else
660+ exit 1
661+ end
662+end
663+
664+if ($0 == __FILE__)
665+ STDOUT.sync = true
666+ STDERR.sync = true
667+ TCPSocket.do_not_reverse_lookup = true
668+ Thread.abort_on_exception = $DEBUG ? true : false
669+
670+ begin
671+ main
672+ rescue Exception => ex
673+ if $logger
674+ log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
675+ else
676+ $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
677+ end
678+ exit 1
679+ end
680+
681+ exit 0
682+end
--- a/changelog
+++ b/changelog
@@ -21,6 +21,13 @@
2121 * [shogi-server]
2222 - Released: Revision "20131215"
2323
24+2013-12-14 Daigo Moriwaki <daigo at debian dot org>
25+
26+ * [usiToCsa]
27+ - Added a new program, bin/usiToCsa.rb, which is a bridge for a
28+ USI engine to connect to the Shogi-server.
29+ - bin/usiToCsa is a sample wrapper script.
30+
2431 2013-12-13 Daigo Moriwaki <daigo at debian dot org>
2532
2633 * [shogi-server]
@@ -54,6 +61,10 @@
5461 factory function name generating a pairing method which will be
5562 used in a specific Floodgate game.
5663 ex. set pairing_factory floodgate_zyunisen
64+=======
65+ - Implemented conversion of move representation between CSA format
66+ and USI one.
67+>>>>>>> 201312-usiToCsa
5768
5869 2013-11-24 Daigo Moriwaki <daigo at debian dot org>
5970
--- a/shogi_server/board.rb
+++ b/shogi_server/board.rb
@@ -94,6 +94,7 @@ EOF
9494 @move_count = move_count
9595 @teban = nil # black => true, white => false
9696 @initial_moves = []
97+ @move = nil
9798 @ous = [nil, nil] # keep OU pieces of Sente and Gote
9899 end
99100 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
@@ -104,6 +105,11 @@ EOF
104105 # moves.
105106 attr_reader :initial_moves
106107
108+ # A move parsed by handle_one_move. If the move is not :normal, the board
109+ # position may or may not be rolled back.
110+ #
111+ attr_reader :move
112+
107113 # See if self equals rhs, including a logical board position (i.e.
108114 # not see object IDs) and sennichite stuff.
109115 #
@@ -678,18 +684,18 @@ EOF
678684 return :illegal # can't put on existing piece
679685 end
680686
681- move = Move.new(x0, y0, x1, y1, name, sente)
682- result = move_to(move)
687+ @move = Move.new(x0, y0, x1, y1, name, sente)
688+ result = move_to(@move)
683689 if (result == :illegal)
684690 # self is unchanged
685691 return :illegal
686692 end
687693 if (checkmated?(sente))
688- move_back(move)
694+ move_back(@move)
689695 return :oute_kaihimore
690696 end
691697 if ((x0 == 0) && (y0 == 0) && (name == "FU") && uchifuzume?(sente))
692- move_back(move)
698+ move_back(@move)
693699 return :uchifuzume
694700 end
695701
@@ -697,12 +703,12 @@ EOF
697703 update_sennichite(sente)
698704 os_result = oute_sennichite?(sente)
699705 if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
700- move_back(move)
706+ move_back(@move)
701707 restore_sennichite_stuff(*sennichite_stuff)
702708 return os_result
703709 end
704710 if sennichite?
705- move_back(move)
711+ move_back(@move)
706712 restore_sennichite_stuff(*sennichite_stuff)
707713 return :sennichite
708714 end
--- a/shogi_server/piece.rb
+++ b/shogi_server/piece.rb
@@ -154,18 +154,17 @@ class Piece
154154 @promoted_name
155155 end
156156
157+ def current_name
158+ return @promoted ? @promoted_name : @name
159+ end
160+
157161 def to_s
158162 if (@sente)
159163 sg = "+"
160164 else
161165 sg = "-"
162166 end
163- if (@promoted)
164- n = @promoted_name
165- else
166- n = @name
167- end
168- return sg + n
167+ return sg + current_name
169168 end
170169 end
171170
--- a/shogi_server/usi.rb
+++ b/shogi_server/usi.rb
@@ -13,7 +13,194 @@ module ShogiServer # for a namespace
1313 gsub("@", "+").
1414 gsub(".", " ")
1515 end
16- end
16+
17+ # 1 -> a
18+ # 2 -> b
19+ # ...
20+ # 9 -> i
21+ def danToAlphabet(int)
22+ return (int+96).chr
23+ end
24+
25+ # a -> 1
26+ # b -> 2
27+ # ...
28+ # i -> 9
29+ def alphabetToDan(s)
30+ if RUBY_VERSION >= "1.9.1"
31+ return s.bytes[0]-96
32+ else
33+ return s[0]-96
34+ end
35+ end
36+
37+ def csaPieceToUsi(csa, sente)
38+ str = ""
39+ case csa
40+ when "FU"
41+ str = "p"
42+ when "KY"
43+ str = "l"
44+ when "KE"
45+ str = "n"
46+ when "GI"
47+ str = "s"
48+ when "KI"
49+ str = "g"
50+ when "KA"
51+ str = "b"
52+ when "HI"
53+ str = "r"
54+ when "OU"
55+ str = "k"
56+ when "TO"
57+ str = "+p"
58+ when "NY"
59+ str = "+l"
60+ when "NK"
61+ str = "+n"
62+ when "NG"
63+ str = "+s"
64+ when "UM"
65+ str = "+b"
66+ when "RY"
67+ str = "+r"
68+ end
69+ return sente ? str.upcase : str
70+ end
71+
72+ def usiPieceToCsa(str)
73+ ret = ""
74+ case str.downcase
75+ when "p"
76+ ret = "FU"
77+ when "l"
78+ ret = "KY"
79+ when "n"
80+ ret = "KE"
81+ when "s"
82+ ret = "GI"
83+ when "g"
84+ ret = "KI"
85+ when "b"
86+ ret = "KA"
87+ when "r"
88+ ret = "HI"
89+ when "+p"
90+ ret = "TO"
91+ when "+l"
92+ ret = "NY"
93+ when "+n"
94+ ret = "NK"
95+ when "+s"
96+ ret = "NG"
97+ when "+b"
98+ ret = "UM"
99+ when "+r"
100+ ret = "RY"
101+ when "k"
102+ ret = "OU"
103+ end
104+ return ret
105+ end
106+
107+ def moveToUsi(move)
108+ str = ""
109+ if move.is_drop?
110+ str += "%s*%s%s" % [csaPieceToUsi(move.name, move.sente).upcase, move.x1, danToAlphabet(move.y1)]
111+ else
112+ str += "%s%s%s%s" % [move.x0, danToAlphabet(move.y0), move.x1, danToAlphabet(move.y1)]
113+ str += "+" if move.promotion
114+ end
115+
116+ return str
117+ end
118+
119+ def usiToCsa(str, board, sente)
120+ ret = ""
121+ if str[1..1] == "*"
122+ # drop
123+ ret += "00%s%s%s" % [str[2..2], alphabetToDan(str[3..3]), usiPieceToCsa(str[0..0])]
124+ else
125+ from_x = str[0..0]
126+ from_y = alphabetToDan(str[1..1])
127+ ret += "%s%s%s%s" % [from_x, from_y, str[2..2], alphabetToDan(str[3..3])]
128+ csa_piece = board.array[from_x.to_i][from_y.to_i]
129+ if str.size == 5 && str[4..4] == "+"
130+ # Promoting move
131+ ret += csa_piece.promoted_name
132+ else
133+ ret += csa_piece.current_name
134+ end
135+ end
136+ return (sente ? "+" : "-") + ret
137+ end
138+ end # class methods
139+
140+ # Convert USI moves to CSA one by one from the initial position
141+ #
142+ class UsiToCsa
143+ attr_reader :board, :csa_moves, :usi_moves
144+
145+ # Constructor
146+ #
147+ def initialize
148+ @board = ShogiServer::Board.new
149+ @board.initial
150+ @sente = true
151+ @csa_moves = []
152+ @usi_moves = []
153+ end
154+
155+ def deep_copy
156+ return Marshal.load(Marshal.dump(self))
157+ end
158+
159+ # Parses a usi move string and returns an array of [move_result_state,
160+ # csa_move_string]
161+ #
162+ def next(usi)
163+ usi_moves << usi
164+ csa = Usi.usiToCsa(usi, @board, @sente)
165+ state = @board.handle_one_move(csa, @sente)
166+ @sente = !@sente
167+ @csa_moves << csa
168+ return [state, csa]
169+ end
170+
171+ end # class UsiToCsa
172+
173+ # Convert CSA moves to USI one by one from the initial position
174+ #
175+ class CsaToUsi
176+ attr_reader :board, :csa_moves, :usi_moves
177+
178+ # Constructor
179+ #
180+ def initialize
181+ @board = ShogiServer::Board.new
182+ @board.initial
183+ @sente = true
184+ @csa_moves = []
185+ @usi_moves = []
186+ end
187+
188+ def deep_copy
189+ return Marshal.load(Marshal.dump(self))
190+ end
191+
192+ # Parses a csa move string and returns an array of [move_result_state,
193+ # usi_move_string]
194+ #
195+ def next(csa)
196+ csa_moves << csa
197+ state = @board.handle_one_move(csa, @sente)
198+ @sente = !@sente
199+ usi = Usi.moveToUsi(@board.move)
200+ @usi_moves << usi
201+ return [state, usi]
202+ end
203+ end # class CsaToUsi
17204
18205 def charToPiece(c)
19206 player = nil
@@ -164,6 +351,7 @@ module ShogiServer # for a namespace
164351 s += hands2usi(board.gote_hands).downcase
165352 return s
166353 end
354+
167355 end # class
168356
169357 end # module
--- a/test/TC_usi.rb
+++ b/test/TC_usi.rb
@@ -84,4 +84,22 @@ P-00FU00FU
8484 EOB
8585 assert_equal "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b 2p", @usi.board2usi(b, b.teban)
8686 end
87+
88+ def test_usiToCsa
89+ # 26th Ryuousen 5th match Moriuchi vs Watanabe on Nov 28th, 2013
90+ usi_moves = %w!7g7f 8c8d 7i6h 3c3d 6g6f 7a6b 5g5f 5c5d 3i4h 3a4b 4i5h 4a3b 6i7h 5a4a 5i6i 6a5b 6h7g 4b3c 8h7i 2b3a 3g3f 4c4d 4h3g 3a6d 5h6g 7c7d 7i6h 5b4c 6i7i 4a3a 7i8h 9c9d 3g4f 6b5c 2i3g 6d7c 1g1f 1c1d 2g2f 3c2d 2h3h 9d9e 1i1h 3a2b 3g2e 4d4e 4f4e 7c1i+ 6h4f 1i4f 4g4f B*5i B*3g 5i3g+ 3h3g B*1i 3g3h 1i4f+ P*4d 4c4d B*7a 4d4c 6g5g 4f5g 7a8b+ P*4d 4e3d 4c3d R*5a 5c4b 5a8a+ S*6i 7h6h 5g6h 3h6h G*5h 8b4f 5h6h 4f6h R*4h G*7i 6i5h+ 6h7h G*6i 7i6i 5h6i B*5g 4h1h+ P*4h 1h2g 5g2d 3d2d 7h6i L*6g S*6h 6g6h+ 6i6h S*5g N*3c 4b3c 2e3c+ 2b3c L*3e P*3d 8a2a G*3a 2a1a 5g6h 7g6h N*6d S*6g 3d3e S*5c 2g3f N*2e 3c4c 5c6d+ 6c6d G*3c 4c5c 3c3b 3a3b N*5e 5d5e 1a5a L*5b L*5d 5c5d 5a5b 5d4e L*4g 3f4g 4h4g 4e3f G*3h!
91+ uc = ShogiServer::Usi::UsiToCsa.new
92+ usi_moves.each do |m|
93+ state, csa = uc.next(m)
94+ assert_equal(:normal, state)
95+ end
96+
97+ cu = ShogiServer::Usi::CsaToUsi.new
98+ uc.csa_moves.each do |m|
99+ state, usi = cu.next(m)
100+ assert_equal(:normal, state)
101+ end
102+
103+ assert_equal(usi_moves, cu.usi_moves)
104+ end
87105 end