OSDN Git Service

fe0ddf4783e97e8a8dbd88a538081dd0e28f1d47
[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 (League::Floodgate.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       else
500         rival = $league.find_rival(@player, @my_sente_str, @game_name)
501         if rival.instance_of?(Symbol)  
502           # An error happened. rival is not a player instance, but an error
503           # symobl that must be returned to the main routine immediately.
504           return rival
505         end
506       end
507
508       if (rival)
509         @player.game_name = @game_name
510         Game::decide_turns(@player, @my_sente_str, rival)
511
512         if (Buoy.game_name?(@game_name))
513           buoy = Buoy.new # TODO config
514           if buoy.is_new_game?(@game_name)
515             # The buoy game is not ready yet.
516             # When the game is set, it will be started.
517             @player.status = "game_waiting"
518           else
519             buoy_game = buoy.get_game(@game_name)
520             if buoy_game.instance_of? NilBuoyGame
521               # error. never reach
522             end
523
524             moves_array = Board::split_moves(buoy_game.moves)
525             board = Board.new
526             begin
527               board.set_from_moves(moves_array)
528             rescue => err
529               # it will never happen since moves have already been checked
530               log_error "Failed to set up a buoy game: #{moves}"
531               return :continue
532             end
533             buoy.decrement_count(buoy_game)
534             Game::new(@player.game_name, @player, rival, board)
535           end
536         else
537           klass = Login.handicapped_game_name?(@game_name) || Board
538           board = klass.new
539           board.initial
540           Game::new(@player.game_name, @player, rival, board)
541         end
542       else # rival not found
543         if (@command_name == "GAME")
544           @player.status = "game_waiting"
545           @player.game_name = @game_name
546           @player.set_sente_from_str(@my_sente_str)
547         else                # challenge
548           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
549           @player.status = "connected"
550           @player.game_name = ""
551           @player.sente = nil
552         end
553       end
554       return :continue
555     end
556   end
557
558   # Command of CHAT
559   #
560   class ChatCommand < Command
561
562     # players array of [name, player]
563     #
564     def initialize(str, player, message, players)
565       super(str, player)
566       @message = message
567       @players = players
568     end
569
570     def call
571       @players.each do |name, p| # TODO player change name
572         if (p.protocol != LoginCSA::PROTOCOL)
573           p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message)) 
574         end
575       end
576       return :continue
577     end
578   end
579
580   # Command of LIST
581   #
582   class ListCommand < Command
583
584     # games array of [game_id, game]
585     #
586     def initialize(str, player, games)
587       super(str, player)
588       @games = games
589     end
590
591     def call
592       buf = Array::new
593       @games.each do |id, game|
594         buf.push(sprintf("##[LIST] %s\n", id))
595       end
596       buf.push("##[LIST] +OK\n")
597       @player.write_safe(buf.join)
598       return :continue
599     end
600   end
601
602   # Command of WHO
603   #
604   class WhoCommand < Command
605
606     # players array of [[name, player]]
607     #
608     def initialize(str, player, players)
609       super(str, player)
610       @players = players
611     end
612
613     def call
614       buf = Array::new
615       @players.each do |name, p|
616         buf.push(sprintf("##[WHO] %s\n", p.to_s))
617       end
618       buf.push("##[WHO] +OK\n")
619       @player.write_safe(buf.join)
620       return :continue
621     end
622   end
623
624   # Command of LOGOUT
625   #
626   class LogoutCommand < Command
627     def initialize(str, player)
628       super
629     end
630
631     def call
632       @player.status = "connected"
633       @player.write_safe("LOGOUT:completed\n")
634       return :return
635     end
636   end
637
638   # Command of CHALLENGE
639   #
640   class ChallengeCommand < Command
641     def initialize(str, player)
642       super
643     end
644
645     def call
646       # This command is only available for CSA's official testing server.
647       # So, this means nothing for this program.
648       @player.write_safe("CHALLENGE ACCEPTED\n")
649       return :continue
650     end
651   end
652
653   # Command for a space
654   #
655   class SpaceCommand < Command
656     def initialize(str, player)
657       super
658     end
659
660     def call
661       ## ignore null string
662       return :continue
663     end
664   end
665
666   # Command for an error
667   #
668   class ErrorCommand < Command
669     def initialize(str, player)
670       super
671       @msg = nil
672     end
673     attr_reader :msg
674
675     def call
676       cmd = @str.chomp
677       # Aim to hide a possible password
678       cmd.gsub!(/LOGIN\s*(\w+)\s+.*/i, 'LOGIN \1...')
679       @msg = "##[ERROR] unknown command %s\n" % [cmd]
680       @player.write_safe(@msg)
681       log_error(@msg)
682       return :continue
683     end
684   end
685
686   #
687   #
688   class SetBuoyCommand < Command
689
690     def initialize(str, player, game_name, moves, count)
691       super(str, player)
692       @game_name = game_name
693       @moves     = moves
694       @count     = count
695     end
696
697     def call
698       unless (Buoy.game_name?(@game_name))
699         @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
700         log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
701         return :continue
702       end
703       buoy = Buoy.new
704       unless buoy.is_new_game?(@game_name)
705         @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
706         log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
707         return :continue
708       end
709       if @count < 1
710         @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
711         log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
712         return :continue
713       end
714
715       # check moves
716       moves_array = Board::split_moves(@moves)
717       board = Board.new
718       begin
719         board.set_from_moves(moves_array)
720       rescue
721         raise WrongMoves
722       end
723
724       buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count)
725       buoy.add_game(buoy_game)
726       @player.write_safe(sprintf("##[SETBUOY] +OK\n"))
727       log_info("A buoy game was created: %s by %s" % [@game_name, @player.name])
728
729       # if two players, who are not @player, are waiting for a new game, start it
730       p1 = $league.get_player("game_waiting", @game_name, true, @player)
731       return :continue unless p1
732       p2 = $league.get_player("game_waiting", @game_name, false, @player)
733       return :continue unless p2
734
735       buoy.decrement_count(buoy_game)
736       game = Game::new(@game_name, p1, p2, board)
737       return :continue
738     rescue WrongMoves => e
739       @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
740       log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
741       return :continue
742     end
743   end
744
745   #
746   #
747   class DeleteBuoyCommand < Command
748     def initialize(str, player, game_name)
749       super(str, player)
750       @game_name = game_name
751     end
752
753     def call
754       buoy = Buoy.new
755       buoy_game = buoy.get_game(@game_name)
756       if buoy_game.instance_of?(NilBuoyGame)
757         @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
758         log_error "Game name not found: %s by %s" % [@game_name, @player.name]
759         return :continue
760       end
761
762       if buoy_game.owner != @player.name
763         @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
764         log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
765         return :continue
766       end
767
768       buoy.delete_game(buoy_game)
769       @player.write_safe(sprintf("##[DELETEBUOY] +OK\n"))
770       log_info("A buoy game was deleted: %s" % [@game_name])
771       return :continue
772     end
773   end
774
775   #
776   #
777   class GetBuoyCountCommand < Command
778     def initialize(str, player, game_name)
779       super(str, player)
780       @game_name = game_name
781     end
782
783     def call
784       buoy = Buoy.new
785       buoy_game = buoy.get_game(@game_name)
786       if buoy_game.instance_of?(NilBuoyGame)
787         @player.write_safe("##[GETBUOYCOUNT] -1\n")
788       else
789         @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
790       end
791       @player.write_safe("##[GETBUOYCOUNT] +OK\n")
792     end
793   end
794
795 end # module ShogiServer