OSDN Git Service

Added test cases.
[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-2008 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 /^%%CHAT\s+(.+)/
73         message = $1
74         cmd = ChatCommand.new(str, player, message, $league.players)
75       when /^%%LIST/
76         cmd = ListCommand.new(str, player, $league.games)
77       when /^%%WHO/
78         cmd = WhoCommand.new(str, player, $league.players)
79       when /^LOGOUT/
80         cmd = LogoutCommand.new(str, player)
81       when /^CHALLENGE/
82         cmd = ChallengeCommand.new(str, player)
83       when /^%%SETBUOY\s+(\S+)\s+(\S+)(.*)/
84         game_name = $1
85         moves     = $2
86         count = 1 # default
87         if $3 && /^\s+(\d*)/ =~ $3
88           count = $1.to_i
89         end
90         cmd = SetBuoyCommand.new(str, player, game_name, moves, count)
91       when /^%%DELETEBUOY\s+(\S+)/
92         game_name = $1
93         cmd = DeleteBuoyCommand.new(str, player, game_name)
94       when /^%%GETBUOYCOUNT\s+(\S+)/
95         game_name = $1
96         cmd = GetBuoyCountCommand.new(str, player, game_name)
97       when /^\s*$/
98         cmd = SpaceCommand.new(str, player)
99       when /^%%%[^%]/
100         # TODO: just ignore commands specific to 81Dojo.
101         # Need to discuss with 81Dojo people.
102         cmd = VoidCommand.new(str, player)
103       else
104         cmd = ErrorCommand.new(str, player)
105       end
106
107       cmd.time = time
108       return cmd
109     end
110
111     def initialize(str, player)
112       @str    = str
113       @player = player
114       @time   = Time.now # this should be replaced later with a real time
115     end
116     attr_accessor :time
117   end
118
119   # Dummy command which does nothing.
120   #
121   class VoidCommand < Command
122     def initialize(str, player)
123       super
124     end
125
126     def call
127       return :continue
128     end
129   end
130
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
134   # yet.
135   #
136   class KeepAliveCommand < Command
137     def initialize(str, player)
138       super
139     end
140
141     def call
142       @player.write_safe("\n")
143       return :continue
144     end
145   end
146
147   # Command of moving a piece.
148   #
149   class MoveCommand < Command
150     def initialize(str, player)
151       super
152     end
153
154     def call
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.")
161           return :continue
162         end
163         additional = array_str.shift
164         comment = nil
165         if /^'(.*)/ =~ additional
166           comment = array_str.unshift("'*#{$1.toeuc}")
167         end
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)
171       end
172       return :continue
173     end
174   end
175
176   # Command like "%TORYO" or :timeout
177   #
178   class SpecialCommand < Command
179     def initialize(str, player)
180       super
181     end
182
183     def call
184       rc = :continue
185       if (@player.status == "game")
186         rc = in_game_status()
187       elsif ["agree_waiting", "start_waiting"].include?(@player.status) 
188         rc = in_waiting_status()
189       else
190         log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].") unless @str == :timeout
191       end
192       return rc
193     end
194
195     def in_game_status
196       rc = :continue
197
198       s = @player.game.handle_one_move(@str, @player, @time)
199       rc = :return if (s && @player.protocol == LoginCSA::PROTOCOL)
200
201       return rc
202     end
203
204     def in_waiting_status
205       rc = :continue
206
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)
211       end
212
213       return rc
214     end
215   end
216
217   # Command of :exception
218   #
219   class ExceptionCommand < Command
220     def initialize(str, player)
221       super
222     end
223
224     def call
225       log_error("Failed to receive a message from #{@player.name}.")
226       return :return
227     end
228   end
229
230   # Command of REJECT
231   #
232   class RejectCommand < Command
233     def initialize(str, player)
234       super
235     end
236
237     def call
238       if (@player.status == "agree_waiting")
239         @player.game.reject(@player.name)
240         return :return if (@player.protocol == LoginCSA::PROTOCOL)
241       else
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))
244       end
245       return :continue
246     end
247   end
248
249   # Command of AGREE
250   #
251   class AgreeCommand < Command
252     def initialize(str, player)
253       super
254     end
255
256     def call
257       if (@player.status == "agree_waiting")
258         @player.status = "start_waiting"
259         if (@player.game.is_startable_status?)
260           @player.game.start
261         end
262       else
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))
265       end
266       return :continue
267     end
268   end
269
270   # Base Command calss requiring a game instance
271   #
272   class BaseCommandForGame < Command
273     def initialize(str, player, game)
274       super(str, player)
275       @game    = game
276       @game_id = game ? game.game_id : nil
277     end
278   end
279
280   # Command of SHOW
281   #
282   class ShowCommand < BaseCommandForGame
283     def initialize(str, player, game)
284       super
285     end
286
287     def call
288       if (@game)
289         @player.write_safe(@game.show.gsub(/^/, '##[SHOW] '))
290       end
291       @player.write_safe("##[SHOW] +OK\n")
292       return :continue
293     end
294   end
295
296   class MonitorHandler
297     def initialize(player)
298       @player = player
299       @type = nil
300       @header = nil
301     end
302     attr_reader :player, :type, :header
303
304     def ==(rhs)
305       return rhs != nil &&
306              rhs.is_a?(MonitorHandler) &&
307              @player == rhs.player &&
308              @type   == rhs.type
309     end
310
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])
314       end
315       @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, "+OK"])
316     end
317   end
318
319   class MonitorHandler1 < MonitorHandler
320     def initialize(player)
321       super
322       @type = 1
323       @header = "MONITOR"
324     end
325
326     def write_one_move(game_id, game)
327       write_safe(game_id, game.show.chomp)
328     end
329   end
330
331   class MonitorHandler2 < MonitorHandler
332     def initialize(player)
333       super
334       @type = 2
335       @header = "MONITOR2"
336     end
337
338     def write_one_move(game_id, game)
339       write_safe(game_id, game.last_move.gsub(",", "\n"))
340     end
341   end
342
343   # Command of MONITORON
344   #
345   class MonitorOnCommand < BaseCommandForGame
346     def initialize(str, player, game)
347       super
348     end
349
350     def call
351       if (@game)
352         monitor_handler = MonitorHandler1.new(@player)
353         @game.monitoron(monitor_handler)
354         monitor_handler.write_safe(@game_id, @game.show)
355       end
356       return :continue
357     end
358   end
359
360   # Command of MONITOROFF
361   #
362   class MonitorOffCommand < BaseCommandForGame
363     def initialize(str, player, game)
364       super
365     end
366
367     def call
368       if (@game)
369         @game.monitoroff(MonitorHandler1.new(@player))
370       end
371       return :continue
372     end
373   end
374
375   # Command of MONITOR2ON
376   #
377   class Monitor2OnCommand < BaseCommandForGame
378     def initialize(str, player, game)
379       super
380     end
381
382     def call
383       if (@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)
388       end
389       return :continue
390     end
391   end
392
393   class Monitor2OffCommand < MonitorOffCommand
394     def initialize(str, player, game)
395       super
396     end
397
398     def call
399       if (@game)
400         @game.monitoroff(MonitorHandler2.new(@player))
401       end
402       return :continue
403     end
404   end
405
406   # Command of HELP
407   #
408   class HelpCommand < Command
409     def initialize(str, player)
410       super
411     end
412
413     def call
414       @player.write_safe(
415         %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
416       return :continue
417     end
418   end
419
420   # Command of RATING
421   #
422   class RatingCommand < Command
423     def initialize(str, player, rated_players)
424       super(str, player)
425       @rated_players = rated_players
426     end
427
428     def call
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")])
432       end
433       @player.write_safe("##[RATING] +OK\n")
434       return :continue
435     end
436   end
437
438   # Command of VERSION
439   #
440   class VersionCommand < Command
441     def initialize(str, player)
442       super
443     end
444
445     def call
446       @player.write_safe "##[VERSION] Shogi Server revision #{ShogiServer::Revision}\n"
447       @player.write_safe("##[VERSION] +OK\n")
448       return :continue
449     end
450   end
451
452   # Command of GAME
453   #
454   class GameCommand < Command
455     def initialize(str, player)
456       super
457     end
458
459     def call
460       if ((@player.status == "connected") || (@player.status == "game_waiting"))
461         @player.status = "connected"
462         @player.game_name = ""
463       else
464         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
465       end
466       return :continue
467     end
468   end
469
470   # Commando of game challenge
471   # TODO make a test case
472   #
473   class GameChallengeCommand < Command
474     def initialize(str, player, command_name, game_name, my_sente_str)
475       super(str, player)
476       @command_name = command_name
477       @game_name    = game_name
478       @my_sente_str = my_sente_str
479     end
480
481     def call
482       if (! Login::good_game_name?(@game_name))
483         @player.write_safe(sprintf("##[ERROR] bad game name\n"))
484         return :continue
485       elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
486         ## continue
487       else
488         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
489         return :continue
490       end
491
492       rival = nil
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))
496           return :continue
497         end
498         @player.sente = nil
499       end # really end
500
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))
504           return :continue
505         end
506         @player.sente = nil
507       else
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
514         else
515           @player.write_safe(sprintf("##[ERROR] bad game option\n"))
516           return :continue
517         end
518       end
519
520       if (rival)
521         @player.game_name = @game_name
522         
523         if ((@my_sente_str == "*") && (rival.sente == nil))
524           if (rand(2) == 0)
525             @player.sente = true
526             rival.sente = false
527           else
528             @player.sente = false
529             rival.sente = true
530           end
531         elsif (rival.sente == true) # rival has higher priority
532           @player.sente = false
533         elsif (rival.sente == false)
534           @player.sente = true
535         elsif (@my_sente_str == "+")
536           @player.sente = true
537           rival.sente = false
538         elsif (@my_sente_str == "-")
539           @player.sente = false
540           rival.sente = true
541         else
542           ## never reached
543         end
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"
550           else
551             buoy_game = buoy.get_game(@game_name)
552             if buoy_game.instance_of? NilBuoyGame
553               # error. never reach
554             end
555
556             moves_array = Board::split_moves(buoy_game.moves)
557             board = Board.new
558             begin
559               board.set_from_moves(moves_array)
560             rescue => err
561               # it will never happen since moves have already been checked
562               log_error "Failed to set up a buoy game: #{moves}"
563               return :continue
564             end
565             buoy.decrement_count(buoy_game)
566             Game::new(@player.game_name, @player, rival, board)
567           end
568         else
569           klass = Login.handicapped_game_name?(@game_name) || Board
570           board = klass.new
571           board.initial
572           Game::new(@player.game_name, @player, rival, board)
573         end
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 == "+")
579             @player.sente = true
580           elsif (@my_sente_str == "-")
581             @player.sente = false
582           else
583             @player.sente = nil
584           end
585         else                # challenge
586           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
587           @player.status = "connected"
588           @player.game_name = ""
589           @player.sente = nil
590         end
591       end
592       return :continue
593     end
594   end
595
596   # Command of CHAT
597   #
598   class ChatCommand < Command
599
600     # players array of [name, player]
601     #
602     def initialize(str, player, message, players)
603       super(str, player)
604       @message = message
605       @players = players
606     end
607
608     def call
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)) 
612         end
613       end
614       return :continue
615     end
616   end
617
618   # Command of LIST
619   #
620   class ListCommand < Command
621
622     # games array of [game_id, game]
623     #
624     def initialize(str, player, games)
625       super(str, player)
626       @games = games
627     end
628
629     def call
630       buf = Array::new
631       @games.each do |id, game|
632         buf.push(sprintf("##[LIST] %s\n", id))
633       end
634       buf.push("##[LIST] +OK\n")
635       @player.write_safe(buf.join)
636       return :continue
637     end
638   end
639
640   # Command of WHO
641   #
642   class WhoCommand < Command
643
644     # players array of [[name, player]]
645     #
646     def initialize(str, player, players)
647       super(str, player)
648       @players = players
649     end
650
651     def call
652       buf = Array::new
653       @players.each do |name, p|
654         buf.push(sprintf("##[WHO] %s\n", p.to_s))
655       end
656       buf.push("##[WHO] +OK\n")
657       @player.write_safe(buf.join)
658       return :continue
659     end
660   end
661
662   # Command of LOGOUT
663   #
664   class LogoutCommand < Command
665     def initialize(str, player)
666       super
667     end
668
669     def call
670       @player.status = "connected"
671       @player.write_safe("LOGOUT:completed\n")
672       return :return
673     end
674   end
675
676   # Command of CHALLENGE
677   #
678   class ChallengeCommand < Command
679     def initialize(str, player)
680       super
681     end
682
683     def call
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")
687       return :continue
688     end
689   end
690
691   # Command for a space
692   #
693   class SpaceCommand < Command
694     def initialize(str, player)
695       super
696     end
697
698     def call
699       ## ignore null string
700       return :continue
701     end
702   end
703
704   # Command for an error
705   #
706   class ErrorCommand < Command
707     def initialize(str, player)
708       super
709     end
710
711     def call
712       msg = "##[ERROR] unknown command %s\n" % [@str.chomp]
713       @player.write_safe(msg)
714       log_error(msg)
715       return :continue
716     end
717   end
718
719   #
720   #
721   class SetBuoyCommand < Command
722
723     def initialize(str, player, game_name, moves, count)
724       super(str, player)
725       @game_name = game_name
726       @moves     = moves
727       @count     = count
728     end
729
730     def call
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]
734         return :continue
735       end
736       buoy = Buoy.new
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]
740         return :continue
741       end
742       if @count < 1
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]
745         return :continue
746       end
747
748       # check moves
749       moves_array = Board::split_moves(@moves)
750       board = Board.new
751       begin
752         board.set_from_moves(moves_array)
753       rescue
754         raise WrongMoves
755       end
756
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])
761
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
767
768       buoy.decrement_count(buoy_game)
769       game = Game::new(@game_name, p1, p2, board)
770       return :continue
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]
774       return :continue
775     end
776   end
777
778   #
779   #
780   class DeleteBuoyCommand < Command
781     def initialize(str, player, game_name)
782       super(str, player)
783       @game_name = game_name
784     end
785
786     def call
787       buoy = Buoy.new
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]
792         return :continue
793       end
794
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]
798         return :continue
799       end
800
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])
804       return :continue
805     end
806   end
807
808   #
809   #
810   class GetBuoyCountCommand < Command
811     def initialize(str, player, game_name)
812       super(str, player)
813       @game_name = game_name
814     end
815
816     def call
817       buoy = Buoy.new
818       buoy_game = buoy.get_game(@game_name)
819       if buoy_game.instance_of?(NilBuoyGame)
820         @player.write_safe("##[GETBUOYCOUNT] -1\n")
821       else
822         @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
823       end
824       @player.write_safe("##[GETBUOYCOUNT] +OK\n")
825     end
826   end
827
828 end # module ShogiServer