3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2008 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)
74 cmd = ChatCommand.new(str, player, message, $league.players)
76 cmd = ListCommand.new(str, player, $league.games)
78 cmd = WhoCommand.new(str, player, $league.players)
80 cmd = LogoutCommand.new(str, player)
82 cmd = ChallengeCommand.new(str, player)
83 when /^%%SETBUOY\s+(\S+)\s+(\S+)(.*)/
87 if $3 && /^\s+(\d*)/ =~ $3
90 cmd = SetBuoyCommand.new(str, player, game_name, moves, count)
91 when /^%%DELETEBUOY\s+(\S+)/
93 cmd = DeleteBuoyCommand.new(str, player, game_name)
94 when /^%%GETBUOYCOUNT\s+(\S+)/
96 cmd = GetBuoyCountCommand.new(str, player, game_name)
98 cmd = SpaceCommand.new(str, player)
100 # TODO: just ignore commands specific to 81Dojo.
101 # Need to discuss with 81Dojo people.
102 cmd = VoidCommand.new(str, player)
104 cmd = ErrorCommand.new(str, player)
111 def initialize(str, player)
114 @time = Time.now # this should be replaced later with a real time
119 # Dummy command which does nothing.
121 class VoidCommand < Command
122 def initialize(str, player)
131 # Application-level protocol for Keep-Alive.
132 # If the server receives an LF, it sends back an LF. Note that the 30 sec
133 # rule (client may not send LF again within 30 sec) is not implemented
136 class KeepAliveCommand < Command
137 def initialize(str, player)
142 @player.write_safe("\n")
147 # Command of moving a piece.
149 class MoveCommand < Command
150 def initialize(str, player)
155 if (@player.status == "game")
156 array_str = @str.split(",")
157 move = array_str.shift
158 if @player.game.last_move &&
159 @player.game.last_move.split(",").first == move
160 log_warning("Received two sequencial identical moves [#{move}] from #{@player.name}. The last one was ignored.")
163 additional = array_str.shift
165 if /^'(.*)/ =~ additional
166 comment = array_str.unshift("'*#{$1.toeuc}")
168 s = @player.game.handle_one_move(move, @player, @time)
169 @player.game.log_game(Kconv.toeuc(comment.first)) if (comment && comment.first && !s)
170 return :return if (s && @player.protocol == LoginCSA::PROTOCOL)
176 # Command like "%TORYO" or :timeout
178 class SpecialCommand < Command
179 def initialize(str, player)
185 if (@player.status == "game")
186 rc = in_game_status()
187 elsif ["agree_waiting", "start_waiting"].include?(@player.status)
188 rc = in_waiting_status()
190 log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].") unless @str == :timeout
198 s = @player.game.handle_one_move(@str, @player, @time)
199 rc = :return if (s && @player.protocol == LoginCSA::PROTOCOL)
204 def in_waiting_status
207 if @player.game.prepared_expire?
208 log_warning("#{@player.status} lasted too long. This play has been expired: %s" % [@player.game.game_id])
209 @player.game.reject("the Server (timed out)")
210 rc = :return if (@player.protocol == LoginCSA::PROTOCOL)
217 # Command of :exception
219 class ExceptionCommand < Command
220 def initialize(str, player)
225 log_error("Failed to receive a message from #{@player.name}.")
232 class RejectCommand < Command
233 def initialize(str, player)
238 if (@player.status == "agree_waiting")
239 @player.game.reject(@player.name)
240 return :return if (@player.protocol == LoginCSA::PROTOCOL)
242 log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
243 @player.write_safe(sprintf("##[ERROR] you are in %s status. REJECT is valid in agree_waiting status\n", @player.status))
251 class AgreeCommand < Command
252 def initialize(str, player)
257 if (@player.status == "agree_waiting")
258 @player.status = "start_waiting"
259 if (@player.game.is_startable_status?)
263 log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
264 @player.write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @player.status))
270 # Base Command calss requiring a game instance
272 class BaseCommandForGame < Command
273 def initialize(str, player, game)
276 @game_id = game ? game.game_id : nil
282 class ShowCommand < BaseCommandForGame
283 def initialize(str, player, game)
289 @player.write_safe(@game.show.gsub(/^/, '##[SHOW] '))
291 @player.write_safe("##[SHOW] +OK\n")
297 def initialize(player)
302 attr_reader :player, :type, :header
306 rhs.is_a?(MonitorHandler) &&
307 @player == rhs.player &&
311 def write_safe(game_id, str)
312 str.chomp.split("\n").each do |line|
313 @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, line.chomp])
315 @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, "+OK"])
319 class MonitorHandler1 < MonitorHandler
320 def initialize(player)
326 def write_one_move(game_id, game)
327 write_safe(game_id, game.show.chomp)
331 class MonitorHandler2 < MonitorHandler
332 def initialize(player)
338 def write_one_move(game_id, game)
339 write_safe(game_id, game.last_move.gsub(",", "\n"))
343 # Command of MONITORON
345 class MonitorOnCommand < BaseCommandForGame
346 def initialize(str, player, game)
352 monitor_handler = MonitorHandler1.new(@player)
353 @game.monitoron(monitor_handler)
354 monitor_handler.write_safe(@game_id, @game.show)
360 # Command of MONITOROFF
362 class MonitorOffCommand < BaseCommandForGame
363 def initialize(str, player, game)
369 @game.monitoroff(MonitorHandler1.new(@player))
375 # Command of MONITOR2ON
377 class Monitor2OnCommand < BaseCommandForGame
378 def initialize(str, player, game)
384 monitor_handler = MonitorHandler2.new(@player)
385 @game.monitoron(monitor_handler)
386 lines = IO::readlines(@game.logfile).join("")
387 monitor_handler.write_safe(@game_id, lines)
393 class Monitor2OffCommand < MonitorOffCommand
394 def initialize(str, player, game)
400 @game.monitoroff(MonitorHandler2.new(@player))
408 class HelpCommand < Command
409 def initialize(str, player)
415 %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
422 class RatingCommand < Command
423 def initialize(str, player, rated_players)
425 @rated_players = rated_players
429 @rated_players.sort {|a,b| b.rate <=> a.rate}.each do |p|
430 @player.write_safe("##[RATING] %s \t %4d @%s\n" %
431 [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
433 @player.write_safe("##[RATING] +OK\n")
440 class VersionCommand < Command
441 def initialize(str, player)
446 @player.write_safe "##[VERSION] Shogi Server revision #{ShogiServer::Revision}\n"
447 @player.write_safe("##[VERSION] +OK\n")
454 class GameCommand < Command
455 def initialize(str, player)
460 if ((@player.status == "connected") || (@player.status == "game_waiting"))
461 @player.status = "connected"
462 @player.game_name = ""
464 @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
470 # Commando of game challenge
471 # TODO make a test case
473 class GameChallengeCommand < Command
474 def initialize(str, player, command_name, game_name, my_sente_str)
476 @command_name = command_name
477 @game_name = game_name
478 @my_sente_str = my_sente_str
482 if (! Login::good_game_name?(@game_name))
483 @player.write_safe(sprintf("##[ERROR] bad game name\n"))
485 elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
488 @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
493 if (Buoy.game_name?(@game_name))
494 if (@my_sente_str != "*")
495 @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
501 if (League::Floodgate.game_name?(@game_name))
502 if (@my_sente_str != "*")
503 @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
508 if (@my_sente_str == "*") && !Login.handicapped_game_name?(@game_name)
509 rival = $league.get_player("game_waiting", @game_name, nil, @player) # no preference
510 elsif (@my_sente_str == "+")
511 rival = $league.get_player("game_waiting", @game_name, false, @player) # rival must be gote
512 elsif (@my_sente_str == "-")
513 rival = $league.get_player("game_waiting", @game_name, true, @player) # rival must be sente
515 @player.write_safe(sprintf("##[ERROR] bad game option\n"))
521 @player.game_name = @game_name
523 if ((@my_sente_str == "*") && (rival.sente == nil))
528 @player.sente = false
531 elsif (rival.sente == true) # rival has higher priority
532 @player.sente = false
533 elsif (rival.sente == false)
535 elsif (@my_sente_str == "+")
538 elsif (@my_sente_str == "-")
539 @player.sente = false
544 if (Buoy.game_name?(@game_name))
545 buoy = Buoy.new # TODO config
546 if buoy.is_new_game?(@game_name)
547 # The buoy game is not ready yet.
548 # When the game is set, it will be started.
549 @player.status = "game_waiting"
551 buoy_game = buoy.get_game(@game_name)
552 if buoy_game.instance_of? NilBuoyGame
556 moves_array = Board::split_moves(buoy_game.moves)
559 board.set_from_moves(moves_array)
561 # it will never happen since moves have already been checked
562 log_error "Failed to set up a buoy game: #{moves}"
565 buoy.decrement_count(buoy_game)
566 Game::new(@player.game_name, @player, rival, board)
569 klass = Login.handicapped_game_name?(@game_name) || Board
572 Game::new(@player.game_name, @player, rival, board)
574 else # rival not found
575 if (@command_name == "GAME")
576 @player.status = "game_waiting"
577 @player.game_name = @game_name
578 if (@my_sente_str == "+")
580 elsif (@my_sente_str == "-")
581 @player.sente = false
586 @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
587 @player.status = "connected"
588 @player.game_name = ""
598 class ChatCommand < Command
600 # players array of [name, player]
602 def initialize(str, player, message, players)
609 @players.each do |name, p| # TODO player change name
610 if (p.protocol != LoginCSA::PROTOCOL)
611 p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message))
620 class ListCommand < Command
622 # games array of [game_id, game]
624 def initialize(str, player, games)
631 @games.each do |id, game|
632 buf.push(sprintf("##[LIST] %s\n", id))
634 buf.push("##[LIST] +OK\n")
635 @player.write_safe(buf.join)
642 class WhoCommand < Command
644 # players array of [[name, player]]
646 def initialize(str, player, players)
653 @players.each do |name, p|
654 buf.push(sprintf("##[WHO] %s\n", p.to_s))
656 buf.push("##[WHO] +OK\n")
657 @player.write_safe(buf.join)
664 class LogoutCommand < Command
665 def initialize(str, player)
670 @player.status = "connected"
671 @player.write_safe("LOGOUT:completed\n")
676 # Command of CHALLENGE
678 class ChallengeCommand < Command
679 def initialize(str, player)
684 # This command is only available for CSA's official testing server.
685 # So, this means nothing for this program.
686 @player.write_safe("CHALLENGE ACCEPTED\n")
691 # Command for a space
693 class SpaceCommand < Command
694 def initialize(str, player)
699 ## ignore null string
704 # Command for an error
706 class ErrorCommand < Command
707 def initialize(str, player)
712 msg = "##[ERROR] unknown command %s\n" % [@str.chomp]
713 @player.write_safe(msg)
721 class SetBuoyCommand < Command
723 def initialize(str, player, game_name, moves, count)
725 @game_name = game_name
731 unless (Buoy.game_name?(@game_name))
732 @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
733 log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
737 unless buoy.is_new_game?(@game_name)
738 @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
739 log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
743 @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
744 log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
749 moves_array = Board::split_moves(@moves)
752 board.set_from_moves(moves_array)
757 buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count)
758 buoy.add_game(buoy_game)
759 @player.write_safe(sprintf("##[SETBUOY] +OK\n"))
760 log_info("A buoy game was created: %s by %s" % [@game_name, @player.name])
762 # if two players, who are not @player, are waiting for a new game, start it
763 p1 = $league.get_player("game_waiting", @game_name, true, @player)
764 return :continue unless p1
765 p2 = $league.get_player("game_waiting", @game_name, false, @player)
766 return :continue unless p2
768 buoy.decrement_count(buoy_game)
769 game = Game::new(@game_name, p1, p2, board)
771 rescue WrongMoves => e
772 @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
773 log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
780 class DeleteBuoyCommand < Command
781 def initialize(str, player, game_name)
783 @game_name = game_name
788 buoy_game = buoy.get_game(@game_name)
789 if buoy_game.instance_of?(NilBuoyGame)
790 @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
791 log_error "Game name not found: %s by %s" % [@game_name, @player.name]
795 if buoy_game.owner != @player.name
796 @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
797 log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
801 buoy.delete_game(buoy_game)
802 @player.write_safe(sprintf("##[DELETEBUOY] +OK\n"))
803 log_info("A buoy game was deleted: %s" % [@game_name])
810 class GetBuoyCountCommand < Command
811 def initialize(str, player, game_name)
813 @game_name = game_name
818 buoy_game = buoy.get_game(@game_name)
819 if buoy_game.instance_of?(NilBuoyGame)
820 @player.write_safe("##[GETBUOYCOUNT] -1\n")
822 @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
824 @player.write_safe("##[GETBUOYCOUNT] +OK\n")
828 end # module ShogiServer