OSDN Git Service

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