OSDN Git Service

b8ffb9b2017e78ea0a901edec9f7807bdcd006f0
[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             options = {:sente_time => buoy_game.sente_time,
544                        :gote_time  => buoy_game.gote_time}
545             Game::new(@player.game_name, @player, rival, board, options)
546           end
547         else
548           klass = Login.handicapped_game_name?(@game_name) || Board
549           board = klass.new
550           board.initial
551           Game::new(@player.game_name, @player, rival, board)
552         end
553       else # rival not found
554         if (@command_name == "GAME")
555           @player.status = "game_waiting"
556           @player.game_name = @game_name
557         else                # challenge
558           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
559           @player.status = "connected"
560           @player.game_name = ""
561           @player.sente = nil
562         end
563       end
564       return :continue
565     end
566   end
567
568   # Command of CHAT
569   #
570   class ChatCommand < Command
571
572     # players array of [name, player]
573     #
574     def initialize(str, player, message, players)
575       super(str, player)
576       @message = message
577       @players = players
578     end
579
580     def call
581       @players.each do |name, p| # TODO player change name
582         if (p.protocol != LoginCSA::PROTOCOL)
583           p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message)) 
584         end
585       end
586       return :continue
587     end
588   end
589
590   # Command of LIST
591   #
592   class ListCommand < Command
593
594     # games array of [game_id, game]
595     #
596     def initialize(str, player, games)
597       super(str, player)
598       @games = games
599     end
600
601     def call
602       buf = Array::new
603       @games.each do |id, game|
604         buf.push(sprintf("##[LIST] %s\n", id))
605       end
606       buf.push("##[LIST] +OK\n")
607       @player.write_safe(buf.join)
608       return :continue
609     end
610   end
611
612   # Command of WHO
613   #
614   class WhoCommand < Command
615
616     # players array of [[name, player]]
617     #
618     def initialize(str, player, players)
619       super(str, player)
620       @players = players
621     end
622
623     def call
624       buf = Array::new
625       @players.each do |name, p|
626         buf.push(sprintf("##[WHO] %s\n", p.to_s))
627       end
628       buf.push("##[WHO] +OK\n")
629       @player.write_safe(buf.join)
630       return :continue
631     end
632   end
633
634   # Command of LOGOUT
635   #
636   class LogoutCommand < Command
637     def initialize(str, player)
638       super
639     end
640
641     def call
642       @player.status = "connected"
643       @player.write_safe("LOGOUT:completed\n")
644       return :return
645     end
646   end
647
648   # Command of CHALLENGE
649   #
650   class ChallengeCommand < Command
651     def initialize(str, player)
652       super
653     end
654
655     def call
656       # This command is only available for CSA's official testing server.
657       # So, this means nothing for this program.
658       @player.write_safe("CHALLENGE ACCEPTED\n")
659       return :continue
660     end
661   end
662
663   # Command for a space
664   #
665   class SpaceCommand < Command
666     def initialize(str, player)
667       super
668     end
669
670     def call
671       ## ignore null string
672       return :continue
673     end
674   end
675
676   # Command for an error
677   #
678   class ErrorCommand < Command
679     def initialize(str, player)
680       super
681       @msg = nil
682     end
683     attr_reader :msg
684
685     def call
686       cmd = @str.chomp
687       # Aim to hide a possible password
688       cmd.gsub!(/LOGIN\s*(\w+)\s+.*/i, 'LOGIN \1...')
689       @msg = "##[ERROR] unknown command %s\n" % [cmd]
690       @player.write_safe(@msg)
691       log_error(@msg)
692       return :continue
693     end
694   end
695
696   #
697   #
698   class SetBuoyCommand < Command
699
700     def initialize(str, player, game_name, moves, count)
701       super(str, player)
702       @game_name = game_name
703       @moves     = moves
704       @count     = count
705       @sente_time = nil
706       @gote_time  = nil
707     end
708
709     # ForkCommand may call this method to set remaining time of both
710     # players.
711     #
712     def set_initial_time(sente_time, gote_time)
713       @sente_time = sente_time
714       @gote_time  = gote_time
715     end
716
717     def call
718       unless (Buoy.game_name?(@game_name))
719         @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
720         log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
721         return :continue
722       end
723       buoy = Buoy.new
724       unless buoy.is_new_game?(@game_name)
725         @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
726         log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
727         return :continue
728       end
729       if @count < 1
730         @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
731         log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
732         return :continue
733       end
734
735       # check moves
736       moves_array = Board::split_moves(@moves)
737       board = Board.new
738       begin
739         board.set_from_moves(moves_array)
740       rescue
741         raise WrongMoves
742       end
743
744       buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count, @sente_time, @gote_time)
745       buoy.add_game(buoy_game)
746       @player.write_safe(sprintf("##[SETBUOY] +OK\n"))
747       log_info("A buoy game was created: %s by %s" % [@game_name, @player.name])
748
749       # if two players are waiting for this buoy game, start it
750       candidates = $league.find_all_players do |player|
751         player.status == "game_waiting" && 
752         player.game_name == @game_name &&
753         player.name != @player.name
754       end
755       if candidates.empty?
756         log_info("No players found for a buoy game. Wait for players: %s" % [@game_name])
757         return :continue 
758       end
759       p1 = candidates.first
760       p2 = $league.find_rival(p1, @game_name)
761       if p2.nil?
762         log_info("No opponent found for a buoy game. Wait for the opponent: %s by %s" % [@game_name, p1.name])
763         return :continue
764       elsif p2.instance_of?(Symbol)  
765         # An error happened. rival is not a player instance, but an error
766         # symobl that must be returned to the main routine immediately.
767         return p2
768       end
769       # found two players: p1 and p2
770       log_info("Starting a buoy game: %s with %s and %s" % [@game_name, p1.name, p2.name])
771       buoy.decrement_count(buoy_game)
772
773       # remaining time for ForkCommand
774       options = {:sente_time => buoy_game.sente_time,
775                  :gote_time  => buoy_game.gote_time}
776       game = Game::new(@game_name, p1, p2, board, options)
777       return :continue
778
779     rescue WrongMoves => e
780       @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
781       log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
782       return :continue
783     end
784   end
785
786   #
787   #
788   class DeleteBuoyCommand < Command
789     def initialize(str, player, game_name)
790       super(str, player)
791       @game_name = game_name
792     end
793
794     def call
795       buoy = Buoy.new
796       buoy_game = buoy.get_game(@game_name)
797       if buoy_game.instance_of?(NilBuoyGame)
798         @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
799         log_error "Game name not found: %s by %s" % [@game_name, @player.name]
800         return :continue
801       end
802
803       if buoy_game.owner != @player.name
804         @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
805         log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
806         return :continue
807       end
808
809       buoy.delete_game(buoy_game)
810       @player.write_safe(sprintf("##[DELETEBUOY] +OK\n"))
811       log_info("A buoy game was deleted: %s" % [@game_name])
812       return :continue
813     end
814   end
815
816   #
817   #
818   class GetBuoyCountCommand < Command
819     def initialize(str, player, game_name)
820       super(str, player)
821       @game_name = game_name
822     end
823
824     def call
825       buoy = Buoy.new
826       buoy_game = buoy.get_game(@game_name)
827       if buoy_game.instance_of?(NilBuoyGame)
828         @player.write_safe("##[GETBUOYCOUNT] -1\n")
829       else
830         @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
831       end
832       @player.write_safe("##[GETBUOYCOUNT] +OK\n")
833     end
834   end
835
836   # %%FORK <source_game> <new_buoy_game> [<nth-move>]
837   # Fork a new game from the posistion where the n-th (starting from 1) move
838   # of a source game is played. The new game should be a valid buoy game
839   # name. The default value of n is the position where the previous position
840   # of the last one.
841   #
842   class ForkCommand < Command
843     def initialize(str, player, source_game, new_buoy_game, nth_move)
844       super(str, player)
845       @source_game   = source_game
846       @new_buoy_game = new_buoy_game
847       @nth_move      = nth_move # may be nil
848     end
849
850     def call
851       game = $league.games[@source_game]
852       unless game
853         @player.write_safe(sprintf("##[ERROR] wrong source game name: %s\n", @source_game))
854         log_error "Received a wrong source game name: %s from %s." % [@source_game, @player.name]
855         return :continue
856       end
857
858       moves = game.read_moves
859       @nth_move = moves.size - 1 unless @nth_move
860       if @nth_move > moves.size or @nth_move < 1
861         @player.write_safe(sprintf("##[ERROR] number of moves to fork is out of range: %s.\n", moves.size))
862         log_error "Number of moves to fork is out of range: %s [%s]" % [@nth_move, @player.name]
863         return :continue
864       end
865       new_moves = moves[0...@nth_move]
866
867       buoy_cmd = SetBuoyCommand.new(@str, @player, @new_buoy_game, new_moves.join(""), 1)
868       buoy_cmd.set_initial_time(game.sente.mytime, game.gote.mytime)
869       return buoy_cmd.call
870     end
871   end
872
873 end # module ShogiServer