3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
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.
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.
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
21 require 'shogi_server'
28 def Command.factory(str, player, time=Time.now)
32 cmd = KeepAliveCommand.new(str, player)
34 cmd = MoveCommand.new(str, player)
35 when /^%[^%]/, :timeout
36 cmd = SpecialCommand.new(str, player)
38 cmd = ExceptionCommand.new(str, player)
40 cmd = RejectCommand.new(str, player)
42 cmd = AgreeCommand.new(str, player)
43 when /^%%SHOW\s+(\S+)/
45 cmd = ShowCommand.new(str, player, $league.games[game_id])
46 when /^%%MONITORON\s+(\S+)/
48 cmd = MonitorOnCommand.new(str, player, $league.games[game_id])
49 when /^%%MONITOROFF\s+(\S+)/
51 cmd = MonitorOffCommand.new(str, player, $league.games[game_id])
52 when /^%%MONITOR2ON\s+(\S+)/
54 cmd = Monitor2OnCommand.new(str, player, $league.games[game_id])
55 when /^%%MONITOR2OFF\s+(\S+)/
57 cmd = Monitor2OffCommand.new(str, player, $league.games[game_id])
59 cmd = HelpCommand.new(str, player)
61 cmd = RatingCommand.new(str, player, $league.rated_players)
63 cmd = VersionCommand.new(str, player)
65 cmd = GameCommand.new(str, player)
66 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
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)
77 cmd = ChatCommand.new(str, player, message, $league.players)
79 cmd = ListCommand.new(str, player, $league.games)
81 cmd = WhoCommand.new(str, player, $league.players)
83 cmd = LogoutCommand.new(str, player)
85 cmd = ChallengeCommand.new(str, player)
86 when /^%%SETBUOY\s+(\S+)\s+(\S+)(.*)/
90 if $3 && /^\s+(\d*)/ =~ $3
93 cmd = SetBuoyCommand.new(str, player, game_name, moves, count)
94 when /^%%DELETEBUOY\s+(\S+)/
96 cmd = DeleteBuoyCommand.new(str, player, game_name)
97 when /^%%GETBUOYCOUNT\s+(\S+)/
99 cmd = GetBuoyCountCommand.new(str, player, game_name)
100 when /^%%FORK\s+(\S+)\s+(\S+)(.*)/
104 if $3 && /^\s+(\d+)/ =~ $3
107 cmd = ForkCommand.new(str, player, source_game, new_buoy_game, nth_move)
108 when /^%%FORK\s+(\S+)$/
112 cmd = ForkCommand.new(str, player, source_game, new_buoy_game, nth_move)
114 cmd = SpaceCommand.new(str, player)
116 # TODO: just ignore commands specific to 81Dojo.
117 # Need to discuss with 81Dojo people.
118 cmd = VoidCommand.new(str, player)
120 cmd = ErrorCommand.new(str, player)
124 player.last_command_at = time
128 def initialize(str, player)
131 @time = Time.now # this should be replaced later with a real time
136 # Dummy command which does nothing.
138 class VoidCommand < Command
139 def initialize(str, player)
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
153 class KeepAliveCommand < Command
154 def initialize(str, player)
159 @player.write_safe("\n")
164 # Command of moving a piece.
166 class MoveCommand < Command
167 def initialize(str, player)
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.")
180 additional = array_str.shift
182 if /^'(.*)/ =~ additional
183 comment = array_str.unshift("'*#{$1.toeuc}")
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)
193 # Command like "%TORYO" or :timeout
195 class SpecialCommand < Command
196 def initialize(str, player)
202 if (@player.status == "game")
203 rc = in_game_status()
204 elsif ["agree_waiting", "start_waiting"].include?(@player.status)
205 rc = in_waiting_status()
207 log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].") unless @str == :timeout
215 s = @player.game.handle_one_move(@str, @player, @time)
216 rc = :return if (s && @player.protocol == LoginCSA::PROTOCOL)
221 def in_waiting_status
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)
234 # Command of :exception
236 class ExceptionCommand < Command
237 def initialize(str, player)
242 log_error("Failed to receive a message from #{@player.name}.")
249 class RejectCommand < Command
250 def initialize(str, player)
255 if (@player.status == "agree_waiting")
256 @player.game.reject(@player.name)
257 return :return if (@player.protocol == LoginCSA::PROTOCOL)
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))
268 class AgreeCommand < Command
269 def initialize(str, player)
274 if (@player.status == "agree_waiting")
275 @player.status = "start_waiting"
276 if (@player.game.is_startable_status?)
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))
287 # Base Command calss requiring a game instance
289 class BaseCommandForGame < Command
290 def initialize(str, player, game)
293 @game_id = game ? game.game_id : nil
299 class ShowCommand < BaseCommandForGame
300 def initialize(str, player, game)
306 @player.write_safe(@game.show.gsub(/^/, '##[SHOW] '))
308 @player.write_safe("##[SHOW] +OK\n")
314 def initialize(player)
319 attr_reader :player, :type, :header
323 rhs.is_a?(MonitorHandler) &&
324 @player == rhs.player &&
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])
332 @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, "+OK"])
336 class MonitorHandler1 < MonitorHandler
337 def initialize(player)
343 def write_one_move(game_id, game)
344 write_safe(game_id, game.show.chomp)
348 class MonitorHandler2 < MonitorHandler
349 def initialize(player)
355 def write_one_move(game_id, game)
356 write_safe(game_id, game.last_move.gsub(",", "\n"))
360 # Command of MONITORON
362 class MonitorOnCommand < BaseCommandForGame
363 def initialize(str, player, game)
369 monitor_handler = MonitorHandler1.new(@player)
370 @game.monitoron(monitor_handler)
371 monitor_handler.write_safe(@game_id, @game.show)
377 # Command of MONITOROFF
379 class MonitorOffCommand < BaseCommandForGame
380 def initialize(str, player, game)
386 @game.monitoroff(MonitorHandler1.new(@player))
392 # Command of MONITOR2ON
394 class Monitor2OnCommand < BaseCommandForGame
395 def initialize(str, player, 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)
410 class Monitor2OffCommand < MonitorOffCommand
411 def initialize(str, player, game)
417 @game.monitoroff(MonitorHandler2.new(@player))
425 class HelpCommand < Command
426 def initialize(str, player)
432 %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
439 class RatingCommand < Command
440 def initialize(str, player, rated_players)
442 @rated_players = rated_players
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")])
450 @player.write_safe("##[RATING] +OK\n")
457 class VersionCommand < Command
458 def initialize(str, player)
463 @player.write_safe "##[VERSION] Shogi Server revision #{ShogiServer::Revision}\n"
464 @player.write_safe("##[VERSION] +OK\n")
471 class GameCommand < Command
472 def initialize(str, player)
477 if ((@player.status == "connected") || (@player.status == "game_waiting"))
478 @player.status = "connected"
479 @player.game_name = ""
481 @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
487 # Commando of game challenge
488 # TODO make a test case
490 class GameChallengeCommand < Command
491 def initialize(str, player, command_name, game_name, my_sente_str)
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)
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")
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)
509 @player.write_safe(sprintf("##[ERROR] invalid identifiers are found or too many characters are used.\n"))
512 @player.write_safe(sprintf("##[ERROR] game name should consist of three parts like game-1500-60.\n"))
515 elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
518 @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
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))
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.
539 @player.game_name = @game_name
540 Game::decide_turns(@player, @my_sente_str, rival)
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"
549 buoy_game = buoy.get_game(@game_name)
550 if buoy_game.instance_of? NilBuoyGame
554 moves_array = Board::split_moves(buoy_game.moves)
557 board.set_from_moves(moves_array)
559 # it will never happen since moves have already been checked
560 log_error "Failed to set up a buoy game: #{moves}"
563 buoy.decrement_count(buoy_game)
564 Game::new(@player.game_name, @player, rival, board)
567 klass = Login.handicapped_game_name?(@game_name) || Board
570 Game::new(@player.game_name, @player, rival, board)
572 else # rival not found
573 if (@command_name == "GAME")
574 @player.status = "game_waiting"
575 @player.game_name = @game_name
577 @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
578 @player.status = "connected"
579 @player.game_name = ""
589 class ChatCommand < Command
591 # players array of [name, player]
593 def initialize(str, player, message, players)
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))
611 class ListCommand < Command
613 # games array of [game_id, game]
615 def initialize(str, player, games)
622 @games.each do |id, game|
623 buf.push(sprintf("##[LIST] %s\n", id))
625 buf.push("##[LIST] +OK\n")
626 @player.write_safe(buf.join)
633 class WhoCommand < Command
635 # players array of [[name, player]]
637 def initialize(str, player, players)
644 @players.each do |name, p|
645 buf.push(sprintf("##[WHO] %s\n", p.to_s))
647 buf.push("##[WHO] +OK\n")
648 @player.write_safe(buf.join)
655 class LogoutCommand < Command
656 def initialize(str, player)
661 @player.status = "connected"
662 @player.write_safe("LOGOUT:completed\n")
667 # Command of CHALLENGE
669 class ChallengeCommand < Command
670 def initialize(str, player)
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")
682 # Command for a space
684 class SpaceCommand < Command
685 def initialize(str, player)
690 ## ignore null string
695 # Command for an error
697 class ErrorCommand < Command
698 def initialize(str, player, msg=nil)
700 @msg = msg || "unknown command"
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)
717 class SetBuoyCommand < Command
719 def initialize(str, player, game_name, moves, count)
721 @game_name = game_name
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]
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]
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]
745 moves_array = Board::split_moves(@moves)
748 board.set_from_moves(moves_array)
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])
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
765 log_info("No players found for a buoy game. Wait for players: %s" % [@game_name])
768 p1 = candidates.first
769 p2 = $league.find_rival(p1, @game_name)
771 log_info("No opponent found for a buoy game. Wait for the opponent: %s by %s" % [@game_name, p1.name])
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.
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)
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]
793 class DeleteBuoyCommand < Command
794 def initialize(str, player, game_name)
796 @game_name = game_name
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]
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]
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])
823 class GetBuoyCountCommand < Command
824 def initialize(str, player, game_name)
826 @game_name = game_name
831 buoy_game = buoy.get_game(@game_name)
832 if buoy_game.instance_of?(NilBuoyGame)
833 @player.write_safe("##[GETBUOYCOUNT] -1\n")
835 @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
837 @player.write_safe("##[GETBUOYCOUNT] +OK\n")
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
848 class ForkCommand < Command
849 def initialize(str, player, source_game, new_buoy_game, nth_move)
851 @source_game = source_game
852 @new_buoy_game = new_buoy_game
853 @nth_move = nth_move # may be nil
855 attr_reader :new_buoy_game
857 def decide_new_buoy_game_name
862 if @source_game.split("+").size >= 2 &&
863 /^([^-]+)-(\d+)-(\d+F?)/ =~ @source_game.split("+")[1]
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]
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")
879 game = $league.games[@source_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]
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]
894 moves[0...@nth_move].each do |m|
895 new_moves_str << m.join(",")
898 unless @new_buoy_game
899 decide_new_buoy_game_name
902 buoy_cmd = SetBuoyCommand.new(@str, @player, @new_buoy_game, new_moves_str, 1)
907 end # module ShogiServer