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)
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])
53 cmd = HelpCommand.new(str, player)
55 cmd = RatingCommand.new(str, player, $league.rated_players)
57 cmd = VersionCommand.new(str, player)
59 cmd = GameCommand.new(str, player)
60 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
64 cmd = GameChallengeCommand.new(str, player,
65 command_name, game_name, my_sente_str)
68 cmd = ChatCommand.new(str, player, message, $league.players)
70 cmd = ListCommand.new(str, player, $league.games)
72 cmd = WhoCommand.new(str, player, $league.players)
74 cmd = LogoutCommand.new(str, player)
76 cmd = ChallengeCommand.new(str, player)
77 when /^%%SETBUOY\s+(\S+)\s+(\S+)(.*)/
81 if $3 && /^\s+(\d*)/ =~ $3
84 cmd = SetBuoyCommand.new(str, player, game_name, moves, count)
85 when /^%%DELETEBUOY\s+(\S+)/
87 cmd = DeleteBuoyCommand.new(str, player, game_name)
88 when /^%%GETBUOYCOUNT\s+(\S+)/
90 cmd = GetBuoyCountCommand.new(str, player, game_name)
92 cmd = SpaceCommand.new(str, player)
94 cmd = ErrorCommand.new(str, player)
100 def initialize(str, player)
106 # Application-level protocol for Keep-Alive.
107 # If the server receives an LF, it sends back an LF. Note that the 30 sec
108 # rule (client may not send LF again within 30 sec) is not implemented
111 class KeepAliveCommand < Command
112 def initialize(str, player)
117 @player.write_safe("\n")
122 # Command of moving a piece.
124 class MoveCommand < Command
125 def initialize(str, player)
130 if (@player.status == "game")
131 array_str = @str.split(",")
132 move = array_str.shift
133 additional = array_str.shift
135 if /^'(.*)/ =~ additional
136 comment = array_str.unshift("'*#{$1.toeuc}")
138 s = @player.game.handle_one_move(move, @player)
139 @player.game.log_game(Kconv.toeuc(comment.first)) if (comment && comment.first && !s)
140 return :return if (s && @player.protocol == LoginCSA::PROTOCOL)
146 # Command like "%TORYO" or :timeout
148 class SpecialCommand < Command
149 def initialize(str, player)
155 if (@player.status == "game")
156 rc = in_game_status()
157 elsif ["agree_waiting", "start_waiting"].include?(@player.status)
158 rc = in_waiting_status()
160 log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].") unless @str == :timeout
168 s = @player.game.handle_one_move(@str, @player)
169 rc = :return if (s && @player.protocol == LoginCSA::PROTOCOL)
174 def in_waiting_status
177 if @player.game.prepared_expire?
178 log_warning("#{@player.status} lasted too long. This play has been expired: %s" % [@player.game.game_id])
179 @player.game.reject("the Server (timed out)")
180 rc = :return if (@player.protocol == LoginCSA::PROTOCOL)
187 # Command of :exception
189 class ExceptionCommand < Command
190 def initialize(str, player)
195 log_error("Failed to receive a message from #{@player.name}.")
202 class RejectCommand < Command
203 def initialize(str, player)
208 if (@player.status == "agree_waiting")
209 @player.game.reject(@player.name)
210 return :return if (@player.protocol == LoginCSA::PROTOCOL)
212 log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
213 @player.write_safe(sprintf("##[ERROR] you are in %s status. REJECT is valid in agree_waiting status\n", @player.status))
221 class AgreeCommand < Command
222 def initialize(str, player)
227 if (@player.status == "agree_waiting")
228 @player.status = "start_waiting"
229 if (@player.game.is_startable_status?)
233 log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
234 @player.write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @player.status))
240 # Base Command calss requiring a game instance
242 class BaseCommandForGame < Command
243 def initialize(str, player, game)
246 @game_id = game ? game.game_id : nil
252 class ShowCommand < BaseCommandForGame
253 def initialize(str, player, game)
259 @player.write_safe(@game.show.gsub(/^/, '##[SHOW] '))
261 @player.write_safe("##[SHOW] +OK\n")
266 # Command of MONITORON
268 class MonitorOnCommand < BaseCommandForGame
269 def initialize(str, player, game)
275 @game.monitoron(@player)
276 @player.write_safe(@game.show.gsub(/^/, "##[MONITOR][#{@game_id}] "))
277 @player.write_safe("##[MONITOR][#{@game_id}] +OK\n")
283 # Command of MONITOROFF
285 class MonitorOffCommand < BaseCommandForGame
286 def initialize(str, player, game)
292 @game.monitoroff(@player)
300 class HelpCommand < Command
301 def initialize(str, player)
307 %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
314 class RatingCommand < Command
315 def initialize(str, player, rated_players)
317 @rated_players = rated_players
321 @rated_players.sort {|a,b| b.rate <=> a.rate}.each do |p|
322 @player.write_safe("##[RATING] %s \t %4d @%s\n" %
323 [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
325 @player.write_safe("##[RATING] +OK\n")
332 class VersionCommand < Command
333 def initialize(str, player)
338 @player.write_safe "##[VERSION] Shogi Server revision #{ShogiServer::Revision}\n"
339 @player.write_safe("##[VERSION] +OK\n")
346 class GameCommand < Command
347 def initialize(str, player)
352 if ((@player.status == "connected") || (@player.status == "game_waiting"))
353 @player.status = "connected"
354 @player.game_name = ""
356 @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
362 # Commando of game challenge
363 # TODO make a test case
365 class GameChallengeCommand < Command
366 def initialize(str, player, command_name, game_name, my_sente_str)
368 @command_name = command_name
369 @game_name = game_name
370 @my_sente_str = my_sente_str
374 if (! Login::good_game_name?(@game_name))
375 @player.write_safe(sprintf("##[ERROR] bad game name\n"))
377 elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
380 @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
385 if (Buoy.game_name?(@game_name))
386 if (@my_sente_str != "*")
387 @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
393 if (League::Floodgate.game_name?(@game_name))
394 if (@my_sente_str != "*")
395 @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
400 if (@my_sente_str == "*")
401 rival = $league.get_player("game_waiting", @game_name, nil, @player) # no preference
402 elsif (@my_sente_str == "+")
403 rival = $league.get_player("game_waiting", @game_name, false, @player) # rival must be gote
404 elsif (@my_sente_str == "-")
405 rival = $league.get_player("game_waiting", @game_name, true, @player) # rival must be sente
408 @player.write_safe(sprintf("##[ERROR] bad game option\n"))
414 @player.game_name = @game_name
416 if ((@my_sente_str == "*") && (rival.sente == nil))
421 @player.sente = false
424 elsif (rival.sente == true) # rival has higher priority
425 @player.sente = false
426 elsif (rival.sente == false)
428 elsif (@my_sente_str == "+")
431 elsif (@my_sente_str == "-")
432 @player.sente = false
437 if (Buoy.game_name?(@game_name))
438 buoy = Buoy.new # TODO config
439 if buoy.is_new_game?(@game_name)
440 # The buoy game is not ready yet.
441 # When the game is set, it will be started.
443 buoy_game = buoy.get_game(@game_name)
444 if buoy_game.instance_of? NilBuoyGame
449 board.set_from_moves(buoy_game.moves)
451 # it will never happen since moves have already been checked
452 log_error "Failed to set up a buoy game: #{moves}"
455 buoy.decrement_count(buoy_game)
456 Game::new(@player.game_name, @player, rival, board)
461 Game::new(@player.game_name, @player, rival, board)
463 else # rival not found
464 if (@command_name == "GAME")
465 @player.status = "game_waiting"
466 @player.game_name = @game_name
467 if (@my_sente_str == "+")
469 elsif (@my_sente_str == "-")
470 @player.sente = false
475 @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
476 @player.status = "connected"
477 @player.game_name = ""
487 class ChatCommand < Command
489 # players array of [name, player]
491 def initialize(str, player, message, players)
498 @players.each do |name, p| # TODO player change name
499 if (p.protocol != LoginCSA::PROTOCOL)
500 p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message))
509 class ListCommand < Command
511 # games array of [game_id, game]
513 def initialize(str, player, games)
520 @games.each do |id, game|
521 buf.push(sprintf("##[LIST] %s\n", id))
523 buf.push("##[LIST] +OK\n")
524 @player.write_safe(buf.join)
531 class WhoCommand < Command
533 # players array of [[name, player]]
535 def initialize(str, player, players)
542 @players.each do |name, p|
543 buf.push(sprintf("##[WHO] %s\n", p.to_s))
545 buf.push("##[WHO] +OK\n")
546 @player.write_safe(buf.join)
553 class LogoutCommand < Command
554 def initialize(str, player)
559 @player.status = "connected"
560 @player.write_safe("LOGOUT:completed\n")
565 # Command of CHALLENGE
567 class ChallengeCommand < Command
568 def initialize(str, player)
573 # This command is only available for CSA's official testing server.
574 # So, this means nothing for this program.
575 @player.write_safe("CHALLENGE ACCEPTED\n")
580 # Command for a space
582 class SpaceCommand < Command
583 def initialize(str, player)
588 ## ignore null string
593 # Command for an error
595 class ErrorCommand < Command
596 def initialize(str, player)
601 msg = "##[ERROR] unknown command %s\n" % [@str]
602 @player.write_safe(msg)
610 class SetBuoyCommand < Command
611 class WrongMoves < ArgumentError; end
613 def initialize(str, player, game_name, moves, count)
615 @game_name = game_name
621 unless (Buoy.game_name?(@game_name))
622 @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
623 log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
627 unless buoy.is_new_game?(@game_name)
628 @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
629 log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
633 @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
634 log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
639 moves_array = split_moves @moves
643 board.set_from_moves(moves_array)
648 buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count)
649 buoy.add_game(buoy_game)
651 # if two players, who are not @player, are waiting for a new game, start it
652 p1 = $league.get_player("game_waiting", @game_name, true, @player)
653 return :continue unless p1
654 p2 = $league.get_player("game_waiting", @game_name, false, @player)
655 return :continue unless p2
657 buoy.decrement_count(buoy_game)
658 game = Game::new(@game_name, p1, p2, board)
660 rescue WrongMoves => e
661 @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
662 log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
668 # Split a moves line into an array of a move string.
669 # If it fails to parse the moves, it raises WrongMoves.
670 # @param moves a moves line. Ex. "+776FU-3334Fu"
671 # @return an array of a move string. Ex. ["+7776FU", "-3334FU"]
673 def split_moves(moves)
676 rs = moves.gsub %r{[\+\-]\d{4}\w{2}} do |s|
680 raise WrongMoves, rs unless rs.empty?
688 class DeleteBuoyCommand < Command
689 def initialize(str, player, game_name)
691 @game_name = game_name
696 buoy_game = buoy.get_game(@game_name)
697 if buoy_game.instance_of?(NilBuoyGame)
698 @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
699 log_error "Game name not found: %s by %s" % [@game_name, @player.name]
703 if buoy_game.owner != @player.name
704 @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
705 log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
709 buoy.delete_game(buoy_game)
710 log_info("A buoy game was deleted: %s" % [@game_name])
717 class GetBuoyCountCommand < Command
718 def initialize(str, player, game_name)
720 @game_name = game_name
725 buoy_game = buoy.get_game(@game_name)
726 if buoy_game.instance_of?(NilBuoyGame)
727 @player.write_safe("##[GETBUOYCOUNT] -1\n")
729 @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
731 @player.write_safe("##[GETBUOYCOUNT] +OK\n")
735 end # module ShogiServer