OSDN Git Service

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