OSDN Git Service

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