OSDN Git Service

Merge remote-tracking branch 'origin/master'
[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 (!ShogiServer::available?)
501         @player.write_safe("##[ERROR] As the service is going to shutdown shortly, starting new games is not allowed now.\n")
502         return :continue
503       elsif (! Login::good_game_name?(@game_name))
504         @player.write_safe(sprintf("##[ERROR] bad game name: %s.\n", @game_name))
505         if (/^(.+)-\d+-\d+F?$/ =~ @game_name)
506           if Login::good_identifier?($1)
507             # do nothing
508           else
509             @player.write_safe(sprintf("##[ERROR] invalid identifiers are found or too many characters are used.\n"))
510           end
511         else
512           @player.write_safe(sprintf("##[ERROR] game name should consist of three parts like game-1500-60.\n"))
513         end
514         return :continue
515       elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
516         ## continue
517       else
518         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
519         return :continue
520       end
521
522       rival = nil
523       if (League::Floodgate.game_name?(@game_name))
524         if (@my_sente_str != "*")
525           @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
526           return :continue
527         end
528         @player.sente = nil
529       else
530         rival = $league.find_rival(@player, @game_name)
531         if rival.instance_of?(Symbol)  
532           # An error happened. rival is not a player instance, but an error
533           # symobl that must be returned to the main routine immediately.
534           return rival
535         end
536       end
537
538       if (rival)
539         @player.game_name = @game_name
540         Game::decide_turns(@player, @my_sente_str, rival)
541
542         if (Buoy.game_name?(@game_name))
543           buoy = Buoy.new # TODO config
544           if buoy.is_new_game?(@game_name)
545             # The buoy game is not ready yet.
546             # When the game is set, it will be started.
547             @player.status = "game_waiting"
548           else
549             buoy_game = buoy.get_game(@game_name)
550             if buoy_game.instance_of? NilBuoyGame
551               # error. never reach
552             end
553
554             moves_array = Board::split_moves(buoy_game.moves)
555             board = Board.new
556             begin
557               board.set_from_moves(moves_array)
558             rescue
559               # it will never happen since moves have already been checked
560               log_error "Failed to set up a buoy game: #{moves}"
561               return :continue
562             end
563             buoy.decrement_count(buoy_game)
564             Game::new(@player.game_name, @player, rival, board)
565           end
566         else
567           klass = Login.handicapped_game_name?(@game_name) || Board
568           board = klass.new
569           board.initial
570           Game::new(@player.game_name, @player, rival, board)
571         end
572       else # rival not found
573         if (@command_name == "GAME")
574           @player.status = "game_waiting"
575           @player.game_name = @game_name
576         else                # challenge
577           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
578           @player.status = "connected"
579           @player.game_name = ""
580           @player.sente = nil
581         end
582       end
583       return :continue
584     end
585   end
586
587   # Command of CHAT
588   #
589   class ChatCommand < Command
590
591     # players array of [name, player]
592     #
593     def initialize(str, player, message, players)
594       super(str, player)
595       @message = message
596       @players = players
597     end
598
599     def call
600       @players.each do |name, p| # TODO player change name
601         if (p.protocol != LoginCSA::PROTOCOL)
602           p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message)) 
603         end
604       end
605       return :continue
606     end
607   end
608
609   # Command of LIST
610   #
611   class ListCommand < Command
612
613     # games array of [game_id, game]
614     #
615     def initialize(str, player, games)
616       super(str, player)
617       @games = games
618     end
619
620     def call
621       buf = Array::new
622       @games.each do |id, game|
623         buf.push(sprintf("##[LIST] %s\n", id))
624       end
625       buf.push("##[LIST] +OK\n")
626       @player.write_safe(buf.join)
627       return :continue
628     end
629   end
630
631   # Command of WHO
632   #
633   class WhoCommand < Command
634
635     # players array of [[name, player]]
636     #
637     def initialize(str, player, players)
638       super(str, player)
639       @players = players
640     end
641
642     def call
643       buf = Array::new
644       @players.each do |name, p|
645         buf.push(sprintf("##[WHO] %s\n", p.to_s))
646       end
647       buf.push("##[WHO] +OK\n")
648       @player.write_safe(buf.join)
649       return :continue
650     end
651   end
652
653   # Command of LOGOUT
654   #
655   class LogoutCommand < Command
656     def initialize(str, player)
657       super
658     end
659
660     def call
661       @player.status = "connected"
662       @player.write_safe("LOGOUT:completed\n")
663       return :return
664     end
665   end
666
667   # Command of CHALLENGE
668   #
669   class ChallengeCommand < Command
670     def initialize(str, player)
671       super
672     end
673
674     def call
675       # This command is only available for CSA's official testing server.
676       # So, this means nothing for this program.
677       @player.write_safe("CHALLENGE ACCEPTED\n")
678       return :continue
679     end
680   end
681
682   # Command for a space
683   #
684   class SpaceCommand < Command
685     def initialize(str, player)
686       super
687     end
688
689     def call
690       ## ignore null string
691       return :continue
692     end
693   end
694
695   # Command for an error
696   #
697   class ErrorCommand < Command
698     def initialize(str, player, msg=nil)
699       super(str, player)
700       @msg = msg || "unknown command"
701     end
702     attr_reader :msg
703
704     def call
705       cmd = @str.chomp
706       # Aim to hide a possible password
707       cmd.gsub!(/LOGIN\s*(\w+)\s+.*/i, 'LOGIN \1...')
708       @msg = "##[ERROR] %s: %s\n" % [@msg, cmd]
709       @player.write_safe(@msg)
710       log_error(@msg)
711       return :continue
712     end
713   end
714
715   #
716   #
717   class SetBuoyCommand < Command
718
719     def initialize(str, player, game_name, moves, count)
720       super(str, player)
721       @game_name = game_name
722       @moves     = moves
723       @count     = count
724     end
725
726     def call
727       unless (Buoy.game_name?(@game_name))
728         @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
729         log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
730         return :continue
731       end
732       buoy = Buoy.new
733       unless buoy.is_new_game?(@game_name)
734         @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
735         log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
736         return :continue
737       end
738       if @count < 1
739         @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
740         log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
741         return :continue
742       end
743
744       # check moves
745       moves_array = Board::split_moves(@moves)
746       board = Board.new
747       begin
748         board.set_from_moves(moves_array)
749       rescue
750         raise WrongMoves
751       end
752
753       buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count)
754       buoy.add_game(buoy_game)
755       @player.write_safe(sprintf("##[SETBUOY] +OK\n"))
756       log_info("A buoy game was created: %s by %s" % [@game_name, @player.name])
757
758       # if two players are waiting for this buoy game, start it
759       candidates = $league.find_all_players do |player|
760         player.status == "game_waiting" && 
761         player.game_name == @game_name &&
762         player.name != @player.name
763       end
764       if candidates.empty?
765         log_info("No players found for a buoy game. Wait for players: %s" % [@game_name])
766         return :continue 
767       end
768       p1 = candidates.first
769       p2 = $league.find_rival(p1, @game_name)
770       if p2.nil?
771         log_info("No opponent found for a buoy game. Wait for the opponent: %s by %s" % [@game_name, p1.name])
772         return :continue
773       elsif p2.instance_of?(Symbol)  
774         # An error happened. rival is not a player instance, but an error
775         # symobl that must be returned to the main routine immediately.
776         return p2
777       end
778       # found two players: p1 and p2
779       log_info("Starting a buoy game: %s with %s and %s" % [@game_name, p1.name, p2.name])
780       buoy.decrement_count(buoy_game)
781       Game::new(@game_name, p1, p2, board)
782       return :continue
783
784     rescue WrongMoves => e
785       @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
786       log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
787       return :continue
788     end
789   end
790
791   #
792   #
793   class DeleteBuoyCommand < Command
794     def initialize(str, player, game_name)
795       super(str, player)
796       @game_name = game_name
797     end
798
799     def call
800       buoy = Buoy.new
801       buoy_game = buoy.get_game(@game_name)
802       if buoy_game.instance_of?(NilBuoyGame)
803         @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
804         log_error "Game name not found: %s by %s" % [@game_name, @player.name]
805         return :continue
806       end
807
808       if buoy_game.owner != @player.name
809         @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
810         log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
811         return :continue
812       end
813
814       buoy.delete_game(buoy_game)
815       @player.write_safe(sprintf("##[DELETEBUOY] +OK\n"))
816       log_info("A buoy game was deleted: %s" % [@game_name])
817       return :continue
818     end
819   end
820
821   #
822   #
823   class GetBuoyCountCommand < Command
824     def initialize(str, player, game_name)
825       super(str, player)
826       @game_name = game_name
827     end
828
829     def call
830       buoy = Buoy.new
831       buoy_game = buoy.get_game(@game_name)
832       if buoy_game.instance_of?(NilBuoyGame)
833         @player.write_safe("##[GETBUOYCOUNT] -1\n")
834       else
835         @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
836       end
837       @player.write_safe("##[GETBUOYCOUNT] +OK\n")
838       return :continue
839     end
840   end
841
842   # %%FORK <source_game> <new_buoy_game> [<nth-move>]
843   # Fork a new game from the posistion where the n-th (starting from 1) move
844   # of a source game is played. The new game should be a valid buoy game
845   # name. The default value of n is the position where the previous position
846   # of the last one.
847   #
848   class ForkCommand < Command
849     def initialize(str, player, source_game, new_buoy_game, nth_move)
850       super(str, player)
851       @source_game   = source_game
852       @new_buoy_game = new_buoy_game
853       @nth_move      = nth_move # may be nil
854     end
855     attr_reader :new_buoy_game
856
857     def decide_new_buoy_game_name
858       name       = nil
859       total_time = nil
860       byo_time   = nil
861
862       if @source_game.split("+").size >= 2 &&
863          /^([^-]+)-(\d+)-(\d+F?)/ =~ @source_game.split("+")[1]
864         name       = $1
865         total_time = $2
866         byo_time   = $3
867       end
868       if name == nil || total_time == nil || byo_time == nil
869         @player.write_safe(sprintf("##[ERROR] wrong source game name to make a new buoy game name: %s\n", @source_game))
870         log_error "Received a wrong source game name to make a new buoy game name: %s from %s." % [@source_game, @player.name]
871         return :continue
872       end
873       @new_buoy_game = "buoy_%s_%d-%s-%s" % [name, @nth_move, total_time, byo_time]
874       @player.write_safe(sprintf("##[FORK]: new buoy game name: %s\n", @new_buoy_game))
875       @player.write_safe("##[FORK] +OK\n")
876     end
877
878     def call
879       game = $league.games[@source_game]
880       unless game
881         @player.write_safe(sprintf("##[ERROR] wrong source game name: %s\n", @source_game))
882         log_error "Received a wrong source game name: %s from %s." % [@source_game, @player.name]
883         return :continue
884       end
885
886       moves = game.read_moves # [["+7776FU","T2"],["-3334FU","T5"]]
887       @nth_move = moves.size - 1 unless @nth_move
888       if @nth_move > moves.size or @nth_move < 1
889         @player.write_safe(sprintf("##[ERROR] number of moves to fork is out of range: %s.\n", moves.size))
890         log_error "Number of moves to fork is out of range: %s [%s]" % [@nth_move, @player.name]
891         return :continue
892       end
893       new_moves_str = ""
894       moves[0...@nth_move].each do |m|
895         new_moves_str << m.join(",")
896       end
897
898       unless @new_buoy_game
899         decide_new_buoy_game_name
900       end
901
902       buoy_cmd = SetBuoyCommand.new(@str, @player, @new_buoy_game, new_moves_str, 1)
903       return buoy_cmd.call
904     end
905   end
906
907 end # module ShogiServer