OSDN Git Service

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