OSDN Git Service

Improved %%FORK command. Thinking times of each move are also provided.
[shogi-server/shogi-server.git] / shogi_server / command.rb
1 ## $Id$
2
3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
5 ##
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
10 ##
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 ## GNU General Public License for more details.
15 ##
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 require 'kconv'
21 require 'shogi_server'
22
23 module ShogiServer
24
25   class Command
26     # Factory method
27     #
28     def Command.factory(str, player, time=Time.now)
29       cmd = nil
30       case str 
31       when "" 
32         cmd = KeepAliveCommand.new(str, player)
33       when /^[\+\-][^%]/
34         cmd = MoveCommand.new(str, player)
35       when /^%[^%]/, :timeout
36         cmd = SpecialCommand.new(str, player)
37       when :exception
38         cmd = ExceptionCommand.new(str, player)
39       when /^REJECT/
40         cmd = RejectCommand.new(str, player)
41       when /^AGREE/
42         cmd = AgreeCommand.new(str, player)
43       when /^%%SHOW\s+(\S+)/
44         game_id = $1
45         cmd = ShowCommand.new(str, player, $league.games[game_id])
46       when /^%%MONITORON\s+(\S+)/
47         game_id = $1
48         cmd = MonitorOnCommand.new(str, player, $league.games[game_id])
49       when /^%%MONITOROFF\s+(\S+)/
50         game_id = $1
51         cmd = MonitorOffCommand.new(str, player, $league.games[game_id])
52       when /^%%MONITOR2ON\s+(\S+)/
53         game_id = $1
54         cmd = Monitor2OnCommand.new(str, player, $league.games[game_id])
55       when /^%%MONITOR2OFF\s+(\S+)/
56         game_id = $1
57         cmd = Monitor2OffCommand.new(str, player, $league.games[game_id])
58       when /^%%HELP/
59         cmd = HelpCommand.new(str, player)
60       when /^%%RATING/
61         cmd = RatingCommand.new(str, player, $league.rated_players)
62       when /^%%VERSION/
63         cmd = VersionCommand.new(str, player)
64       when /^%%GAME\s*$/
65         cmd = GameCommand.new(str, player)
66       when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
67         command_name = $1
68         game_name = $2
69         my_sente_str = $3
70         cmd = GameChallengeCommand.new(str, player, 
71                                        command_name, game_name, my_sente_str)
72       when /^%%CHAT\s+(.+)/
73         message = $1
74         cmd = ChatCommand.new(str, player, message, $league.players)
75       when /^%%LIST/
76         cmd = ListCommand.new(str, player, $league.games)
77       when /^%%WHO/
78         cmd = WhoCommand.new(str, player, $league.players)
79       when /^LOGOUT/
80         cmd = LogoutCommand.new(str, player)
81       when /^CHALLENGE/
82         cmd = ChallengeCommand.new(str, player)
83       when /^%%SETBUOY\s+(\S+)\s+(\S+)(.*)/
84         game_name = $1
85         moves     = $2
86         count = 1 # default
87         if $3 && /^\s+(\d*)/ =~ $3
88           count = $1.to_i
89         end
90         cmd = SetBuoyCommand.new(str, player, game_name, moves, count)
91       when /^%%DELETEBUOY\s+(\S+)/
92         game_name = $1
93         cmd = DeleteBuoyCommand.new(str, player, game_name)
94       when /^%%GETBUOYCOUNT\s+(\S+)/
95         game_name = $1
96         cmd = GetBuoyCountCommand.new(str, player, game_name)
97       when /^%%FORK\s+(\S+)\s+(\S+)(.*)/
98         source_game   = $1
99         new_buoy_game = $2
100         nth_move      = nil
101         if $3 && /^\s+(\d+)/ =~ $3
102           nth_move = $3.to_i
103         end
104         cmd = ForkCommand.new(str, player, source_game, new_buoy_game, nth_move)
105       when /^\s*$/
106         cmd = SpaceCommand.new(str, player)
107       when /^%%%[^%]/
108         # TODO: just ignore commands specific to 81Dojo.
109         # Need to discuss with 81Dojo people.
110         cmd = VoidCommand.new(str, player)
111       else
112         cmd = ErrorCommand.new(str, player)
113       end
114
115       cmd.time = time
116       return cmd
117     end
118
119     def initialize(str, player)
120       @str    = str
121       @player = player
122       @time   = Time.now # this should be replaced later with a real time
123     end
124     attr_accessor :time
125   end
126
127   # Dummy command which does nothing.
128   #
129   class VoidCommand < Command
130     def initialize(str, player)
131       super
132     end
133
134     def call
135       return :continue
136     end
137   end
138
139   # Application-level protocol for Keep-Alive.
140   # If the server receives an LF, it sends back an LF.  Note that the 30 sec
141   # rule (client may not send LF again within 30 sec) is not implemented
142   # yet.
143   #
144   class KeepAliveCommand < Command
145     def initialize(str, player)
146       super
147     end
148
149     def call
150       @player.write_safe("\n")
151       return :continue
152     end
153   end
154
155   # Command of moving a piece.
156   #
157   class MoveCommand < Command
158     def initialize(str, player)
159       super
160     end
161
162     def call
163       if (@player.status == "game")
164         array_str = @str.split(",")
165         move = array_str.shift
166         if @player.game.last_move &&
167            @player.game.last_move.split(",").first == move
168           log_warning("Received two sequencial identical moves [#{move}] from #{@player.name}. The last one was ignored.")
169           return :continue
170         end
171         additional = array_str.shift
172         comment = nil
173         if /^'(.*)/ =~ additional
174           comment = array_str.unshift("'*#{$1.toeuc}")
175         end
176         s = @player.game.handle_one_move(move, @player, @time)
177         @player.game.log_game(Kconv.toeuc(comment.first)) if (comment && comment.first && !s)
178         return :return if (s && @player.protocol == LoginCSA::PROTOCOL)
179       end
180       return :continue
181     end
182   end
183
184   # Command like "%TORYO" or :timeout
185   #
186   class SpecialCommand < Command
187     def initialize(str, player)
188       super
189     end
190
191     def call
192       rc = :continue
193       if (@player.status == "game")
194         rc = in_game_status()
195       elsif ["agree_waiting", "start_waiting"].include?(@player.status) 
196         rc = in_waiting_status()
197       else
198         log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].") unless @str == :timeout
199       end
200       return rc
201     end
202
203     def in_game_status
204       rc = :continue
205
206       s = @player.game.handle_one_move(@str, @player, @time)
207       rc = :return if (s && @player.protocol == LoginCSA::PROTOCOL)
208
209       return rc
210     end
211
212     def in_waiting_status
213       rc = :continue
214
215       if @player.game.prepared_expire?
216         log_warning("#{@player.status} lasted too long. This play has been expired: %s" % [@player.game.game_id])
217         @player.game.reject("the Server (timed out)")
218         rc = :return if (@player.protocol == LoginCSA::PROTOCOL)
219       end
220
221       return rc
222     end
223   end
224
225   # Command of :exception
226   #
227   class ExceptionCommand < Command
228     def initialize(str, player)
229       super
230     end
231
232     def call
233       log_error("Failed to receive a message from #{@player.name}.")
234       return :return
235     end
236   end
237
238   # Command of REJECT
239   #
240   class RejectCommand < Command
241     def initialize(str, player)
242       super
243     end
244
245     def call
246       if (@player.status == "agree_waiting")
247         @player.game.reject(@player.name)
248         return :return if (@player.protocol == LoginCSA::PROTOCOL)
249       else
250         log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
251         @player.write_safe(sprintf("##[ERROR] you are in %s status. REJECT is valid in agree_waiting status\n", @player.status))
252       end
253       return :continue
254     end
255   end
256
257   # Command of AGREE
258   #
259   class AgreeCommand < Command
260     def initialize(str, player)
261       super
262     end
263
264     def call
265       if (@player.status == "agree_waiting")
266         @player.status = "start_waiting"
267         if (@player.game.is_startable_status?)
268           @player.game.start
269         end
270       else
271         log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
272         @player.write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @player.status))
273       end
274       return :continue
275     end
276   end
277
278   # Base Command calss requiring a game instance
279   #
280   class BaseCommandForGame < Command
281     def initialize(str, player, game)
282       super(str, player)
283       @game    = game
284       @game_id = game ? game.game_id : nil
285     end
286   end
287
288   # Command of SHOW
289   #
290   class ShowCommand < BaseCommandForGame
291     def initialize(str, player, game)
292       super
293     end
294
295     def call
296       if (@game)
297         @player.write_safe(@game.show.gsub(/^/, '##[SHOW] '))
298       end
299       @player.write_safe("##[SHOW] +OK\n")
300       return :continue
301     end
302   end
303
304   class MonitorHandler
305     def initialize(player)
306       @player = player
307       @type = nil
308       @header = nil
309     end
310     attr_reader :player, :type, :header
311
312     def ==(rhs)
313       return rhs != nil &&
314              rhs.is_a?(MonitorHandler) &&
315              @player == rhs.player &&
316              @type   == rhs.type
317     end
318
319     def write_safe(game_id, str)
320       str.chomp.split("\n").each do |line|
321         @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, line.chomp])
322       end
323       @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, "+OK"])
324     end
325   end
326
327   class MonitorHandler1 < MonitorHandler
328     def initialize(player)
329       super
330       @type = 1
331       @header = "MONITOR"
332     end
333
334     def write_one_move(game_id, game)
335       write_safe(game_id, game.show.chomp)
336     end
337   end
338
339   class MonitorHandler2 < MonitorHandler
340     def initialize(player)
341       super
342       @type = 2
343       @header = "MONITOR2"
344     end
345
346     def write_one_move(game_id, game)
347       write_safe(game_id, game.last_move.gsub(",", "\n"))
348     end
349   end
350
351   # Command of MONITORON
352   #
353   class MonitorOnCommand < BaseCommandForGame
354     def initialize(str, player, game)
355       super
356     end
357
358     def call
359       if (@game)
360         monitor_handler = MonitorHandler1.new(@player)
361         @game.monitoron(monitor_handler)
362         monitor_handler.write_safe(@game_id, @game.show)
363       end
364       return :continue
365     end
366   end
367
368   # Command of MONITOROFF
369   #
370   class MonitorOffCommand < BaseCommandForGame
371     def initialize(str, player, game)
372       super
373     end
374
375     def call
376       if (@game)
377         @game.monitoroff(MonitorHandler1.new(@player))
378       end
379       return :continue
380     end
381   end
382
383   # Command of MONITOR2ON
384   #
385   class Monitor2OnCommand < BaseCommandForGame
386     def initialize(str, player, game)
387       super
388     end
389
390     def call
391       if (@game)
392         monitor_handler = MonitorHandler2.new(@player)
393         @game.monitoron(monitor_handler)
394         lines = IO::readlines(@game.logfile).join("")
395         monitor_handler.write_safe(@game_id, lines)
396       end
397       return :continue
398     end
399   end
400
401   class Monitor2OffCommand < MonitorOffCommand
402     def initialize(str, player, game)
403       super
404     end
405
406     def call
407       if (@game)
408         @game.monitoroff(MonitorHandler2.new(@player))
409       end
410       return :continue
411     end
412   end
413
414   # Command of HELP
415   #
416   class HelpCommand < Command
417     def initialize(str, player)
418       super
419     end
420
421     def call
422       @player.write_safe(
423         %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
424       return :continue
425     end
426   end
427
428   # Command of RATING
429   #
430   class RatingCommand < Command
431     def initialize(str, player, rated_players)
432       super(str, player)
433       @rated_players = rated_players
434     end
435
436     def call
437       @rated_players.sort {|a,b| b.rate <=> a.rate}.each do |p|
438         @player.write_safe("##[RATING] %s \t %4d @%s\n" % 
439                    [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
440       end
441       @player.write_safe("##[RATING] +OK\n")
442       return :continue
443     end
444   end
445
446   # Command of VERSION
447   #
448   class VersionCommand < Command
449     def initialize(str, player)
450       super
451     end
452
453     def call
454       @player.write_safe "##[VERSION] Shogi Server revision #{ShogiServer::Revision}\n"
455       @player.write_safe("##[VERSION] +OK\n")
456       return :continue
457     end
458   end
459
460   # Command of GAME
461   #
462   class GameCommand < Command
463     def initialize(str, player)
464       super
465     end
466
467     def call
468       if ((@player.status == "connected") || (@player.status == "game_waiting"))
469         @player.status = "connected"
470         @player.game_name = ""
471       else
472         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
473       end
474       return :continue
475     end
476   end
477
478   # Commando of game challenge
479   # TODO make a test case
480   #
481   class GameChallengeCommand < Command
482     def initialize(str, player, command_name, game_name, my_sente_str)
483       super(str, player)
484       @command_name = command_name
485       @game_name    = game_name
486       @my_sente_str = my_sente_str
487       player.set_sente_from_str(@my_sente_str)
488     end
489
490     def call
491       if (! Login::good_game_name?(@game_name))
492         @player.write_safe(sprintf("##[ERROR] bad game name\n"))
493         return :continue
494       elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
495         ## continue
496       else
497         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
498         return :continue
499       end
500
501       rival = nil
502       if (League::Floodgate.game_name?(@game_name))
503         if (@my_sente_str != "*")
504           @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
505           return :continue
506         end
507         @player.sente = nil
508       else
509         rival = $league.find_rival(@player, @game_name)
510         if rival.instance_of?(Symbol)  
511           # An error happened. rival is not a player instance, but an error
512           # symobl that must be returned to the main routine immediately.
513           return rival
514         end
515       end
516
517       if (rival)
518         @player.game_name = @game_name
519         Game::decide_turns(@player, @my_sente_str, rival)
520
521         if (Buoy.game_name?(@game_name))
522           buoy = Buoy.new # TODO config
523           if buoy.is_new_game?(@game_name)
524             # The buoy game is not ready yet.
525             # When the game is set, it will be started.
526             @player.status = "game_waiting"
527           else
528             buoy_game = buoy.get_game(@game_name)
529             if buoy_game.instance_of? NilBuoyGame
530               # error. never reach
531             end
532
533             moves_array = Board::split_moves(buoy_game.moves)
534             board = Board.new
535             begin
536               board.set_from_moves(moves_array)
537             rescue => err
538               # it will never happen since moves have already been checked
539               log_error "Failed to set up a buoy game: #{moves}"
540               return :continue
541             end
542             buoy.decrement_count(buoy_game)
543             Game::new(@player.game_name, @player, rival, board)
544           end
545         else
546           klass = Login.handicapped_game_name?(@game_name) || Board
547           board = klass.new
548           board.initial
549           Game::new(@player.game_name, @player, rival, board)
550         end
551       else # rival not found
552         if (@command_name == "GAME")
553           @player.status = "game_waiting"
554           @player.game_name = @game_name
555         else                # challenge
556           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
557           @player.status = "connected"
558           @player.game_name = ""
559           @player.sente = nil
560         end
561       end
562       return :continue
563     end
564   end
565
566   # Command of CHAT
567   #
568   class ChatCommand < Command
569
570     # players array of [name, player]
571     #
572     def initialize(str, player, message, players)
573       super(str, player)
574       @message = message
575       @players = players
576     end
577
578     def call
579       @players.each do |name, p| # TODO player change name
580         if (p.protocol != LoginCSA::PROTOCOL)
581           p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message)) 
582         end
583       end
584       return :continue
585     end
586   end
587
588   # Command of LIST
589   #
590   class ListCommand < Command
591
592     # games array of [game_id, game]
593     #
594     def initialize(str, player, games)
595       super(str, player)
596       @games = games
597     end
598
599     def call
600       buf = Array::new
601       @games.each do |id, game|
602         buf.push(sprintf("##[LIST] %s\n", id))
603       end
604       buf.push("##[LIST] +OK\n")
605       @player.write_safe(buf.join)
606       return :continue
607     end
608   end
609
610   # Command of WHO
611   #
612   class WhoCommand < Command
613
614     # players array of [[name, player]]
615     #
616     def initialize(str, player, players)
617       super(str, player)
618       @players = players
619     end
620
621     def call
622       buf = Array::new
623       @players.each do |name, p|
624         buf.push(sprintf("##[WHO] %s\n", p.to_s))
625       end
626       buf.push("##[WHO] +OK\n")
627       @player.write_safe(buf.join)
628       return :continue
629     end
630   end
631
632   # Command of LOGOUT
633   #
634   class LogoutCommand < Command
635     def initialize(str, player)
636       super
637     end
638
639     def call
640       @player.status = "connected"
641       @player.write_safe("LOGOUT:completed\n")
642       return :return
643     end
644   end
645
646   # Command of CHALLENGE
647   #
648   class ChallengeCommand < Command
649     def initialize(str, player)
650       super
651     end
652
653     def call
654       # This command is only available for CSA's official testing server.
655       # So, this means nothing for this program.
656       @player.write_safe("CHALLENGE ACCEPTED\n")
657       return :continue
658     end
659   end
660
661   # Command for a space
662   #
663   class SpaceCommand < Command
664     def initialize(str, player)
665       super
666     end
667
668     def call
669       ## ignore null string
670       return :continue
671     end
672   end
673
674   # Command for an error
675   #
676   class ErrorCommand < Command
677     def initialize(str, player)
678       super
679       @msg = nil
680     end
681     attr_reader :msg
682
683     def call
684       cmd = @str.chomp
685       # Aim to hide a possible password
686       cmd.gsub!(/LOGIN\s*(\w+)\s+.*/i, 'LOGIN \1...')
687       @msg = "##[ERROR] unknown command %s\n" % [cmd]
688       @player.write_safe(@msg)
689       log_error(@msg)
690       return :continue
691     end
692   end
693
694   #
695   #
696   class SetBuoyCommand < Command
697
698     def initialize(str, player, game_name, moves, count)
699       super(str, player)
700       @game_name = game_name
701       @moves     = moves
702       @count     = count
703     end
704
705     def call
706       unless (Buoy.game_name?(@game_name))
707         @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
708         log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
709         return :continue
710       end
711       buoy = Buoy.new
712       unless buoy.is_new_game?(@game_name)
713         @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
714         log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
715         return :continue
716       end
717       if @count < 1
718         @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
719         log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
720         return :continue
721       end
722
723       # check moves
724       moves_array = Board::split_moves(@moves)
725       board = Board.new
726       begin
727         board.set_from_moves(moves_array)
728       rescue
729         raise WrongMoves
730       end
731
732       buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count)
733       buoy.add_game(buoy_game)
734       @player.write_safe(sprintf("##[SETBUOY] +OK\n"))
735       log_info("A buoy game was created: %s by %s" % [@game_name, @player.name])
736
737       # if two players are waiting for this buoy game, start it
738       candidates = $league.find_all_players do |player|
739         player.status == "game_waiting" && 
740         player.game_name == @game_name &&
741         player.name != @player.name
742       end
743       if candidates.empty?
744         log_info("No players found for a buoy game. Wait for players: %s" % [@game_name])
745         return :continue 
746       end
747       p1 = candidates.first
748       p2 = $league.find_rival(p1, @game_name)
749       if p2.nil?
750         log_info("No opponent found for a buoy game. Wait for the opponent: %s by %s" % [@game_name, p1.name])
751         return :continue
752       elsif p2.instance_of?(Symbol)  
753         # An error happened. rival is not a player instance, but an error
754         # symobl that must be returned to the main routine immediately.
755         return p2
756       end
757       # found two players: p1 and p2
758       log_info("Starting a buoy game: %s with %s and %s" % [@game_name, p1.name, p2.name])
759       buoy.decrement_count(buoy_game)
760       game = Game::new(@game_name, p1, p2, board)
761       return :continue
762
763     rescue WrongMoves => e
764       @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
765       log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
766       return :continue
767     end
768   end
769
770   #
771   #
772   class DeleteBuoyCommand < Command
773     def initialize(str, player, game_name)
774       super(str, player)
775       @game_name = game_name
776     end
777
778     def call
779       buoy = Buoy.new
780       buoy_game = buoy.get_game(@game_name)
781       if buoy_game.instance_of?(NilBuoyGame)
782         @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
783         log_error "Game name not found: %s by %s" % [@game_name, @player.name]
784         return :continue
785       end
786
787       if buoy_game.owner != @player.name
788         @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
789         log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
790         return :continue
791       end
792
793       buoy.delete_game(buoy_game)
794       @player.write_safe(sprintf("##[DELETEBUOY] +OK\n"))
795       log_info("A buoy game was deleted: %s" % [@game_name])
796       return :continue
797     end
798   end
799
800   #
801   #
802   class GetBuoyCountCommand < Command
803     def initialize(str, player, game_name)
804       super(str, player)
805       @game_name = game_name
806     end
807
808     def call
809       buoy = Buoy.new
810       buoy_game = buoy.get_game(@game_name)
811       if buoy_game.instance_of?(NilBuoyGame)
812         @player.write_safe("##[GETBUOYCOUNT] -1\n")
813       else
814         @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
815       end
816       @player.write_safe("##[GETBUOYCOUNT] +OK\n")
817     end
818   end
819
820   # %%FORK <source_game> <new_buoy_game> [<nth-move>]
821   # Fork a new game from the posistion where the n-th (starting from 1) move
822   # of a source game is played. The new game should be a valid buoy game
823   # name. The default value of n is the position where the previous position
824   # of the last one.
825   #
826   class ForkCommand < Command
827     def initialize(str, player, source_game, new_buoy_game, nth_move)
828       super(str, player)
829       @source_game   = source_game
830       @new_buoy_game = new_buoy_game
831       @nth_move      = nth_move # may be nil
832     end
833
834     def call
835       game = $league.games[@source_game]
836       unless game
837         @player.write_safe(sprintf("##[ERROR] wrong source game name: %s\n", @source_game))
838         log_error "Received a wrong source game name: %s from %s." % [@source_game, @player.name]
839         return :continue
840       end
841
842       moves = game.read_moves # [["+7776FU","T2"],["-3334FU","T5"]]
843       @nth_move = moves.size - 1 unless @nth_move
844       if @nth_move > moves.size or @nth_move < 1
845         @player.write_safe(sprintf("##[ERROR] number of moves to fork is out of range: %s.\n", moves.size))
846         log_error "Number of moves to fork is out of range: %s [%s]" % [@nth_move, @player.name]
847         return :continue
848       end
849       new_moves_str = ""
850       moves[0...@nth_move].each do |m|
851         new_moves_str << m.join(",")
852       end
853       buoy_cmd = SetBuoyCommand.new(@str, @player, @new_buoy_game, new_moves_str, 1)
854       return buoy_cmd.call
855     end
856   end
857
858 end # module ShogiServer