OSDN Git Service

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