OSDN Git Service

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