OSDN Git Service

* [shogi-server] Ignore the last move of two sequential ones
[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 # same
378     def initialize(str, player, game)
379       super
380     end
381   end
382
383   # Command of HELP
384   #
385   class HelpCommand < Command
386     def initialize(str, player)
387       super
388     end
389
390     def call
391       @player.write_safe(
392         %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
393       return :continue
394     end
395   end
396
397   # Command of RATING
398   #
399   class RatingCommand < Command
400     def initialize(str, player, rated_players)
401       super(str, player)
402       @rated_players = rated_players
403     end
404
405     def call
406       @rated_players.sort {|a,b| b.rate <=> a.rate}.each do |p|
407         @player.write_safe("##[RATING] %s \t %4d @%s\n" % 
408                    [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
409       end
410       @player.write_safe("##[RATING] +OK\n")
411       return :continue
412     end
413   end
414
415   # Command of VERSION
416   #
417   class VersionCommand < Command
418     def initialize(str, player)
419       super
420     end
421
422     def call
423       @player.write_safe "##[VERSION] Shogi Server revision #{ShogiServer::Revision}\n"
424       @player.write_safe("##[VERSION] +OK\n")
425       return :continue
426     end
427   end
428
429   # Command of GAME
430   #
431   class GameCommand < Command
432     def initialize(str, player)
433       super
434     end
435
436     def call
437       if ((@player.status == "connected") || (@player.status == "game_waiting"))
438         @player.status = "connected"
439         @player.game_name = ""
440       else
441         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
442       end
443       return :continue
444     end
445   end
446
447   # Commando of game challenge
448   # TODO make a test case
449   #
450   class GameChallengeCommand < Command
451     def initialize(str, player, command_name, game_name, my_sente_str)
452       super(str, player)
453       @command_name = command_name
454       @game_name    = game_name
455       @my_sente_str = my_sente_str
456     end
457
458     def call
459       if (! Login::good_game_name?(@game_name))
460         @player.write_safe(sprintf("##[ERROR] bad game name\n"))
461         return :continue
462       elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
463         ## continue
464       else
465         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
466         return :continue
467       end
468
469       rival = nil
470       if (Buoy.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       end # really end
477
478       if (League::Floodgate.game_name?(@game_name))
479         if (@my_sente_str != "*")
480           @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
481           return :continue
482         end
483         @player.sente = nil
484       else
485         if (@my_sente_str == "*") && !Login.handicapped_game_name?(@game_name)
486           rival = $league.get_player("game_waiting", @game_name, nil, @player) # no preference
487         elsif (@my_sente_str == "+")
488           rival = $league.get_player("game_waiting", @game_name, false, @player) # rival must be gote
489         elsif (@my_sente_str == "-")
490           rival = $league.get_player("game_waiting", @game_name, true, @player) # rival must be sente
491         else
492           @player.write_safe(sprintf("##[ERROR] bad game option\n"))
493           return :continue
494         end
495       end
496
497       if (rival)
498         @player.game_name = @game_name
499         
500         if ((@my_sente_str == "*") && (rival.sente == nil))
501           if (rand(2) == 0)
502             @player.sente = true
503             rival.sente = false
504           else
505             @player.sente = false
506             rival.sente = true
507           end
508         elsif (rival.sente == true) # rival has higher priority
509           @player.sente = false
510         elsif (rival.sente == false)
511           @player.sente = true
512         elsif (@my_sente_str == "+")
513           @player.sente = true
514           rival.sente = false
515         elsif (@my_sente_str == "-")
516           @player.sente = false
517           rival.sente = true
518         else
519           ## never reached
520         end
521         if (Buoy.game_name?(@game_name))
522           buoy = Buoy.new # TODO config
523           if buoy.is_new_game?(@game_name)
524             # The buoy game is not ready yet.
525             # When the game is set, it will be started.
526             @player.status = "game_waiting"
527           else
528             buoy_game = buoy.get_game(@game_name)
529             if buoy_game.instance_of? NilBuoyGame
530               # error. never reach
531             end
532
533             moves_array = Board::split_moves(buoy_game.moves)
534             board = Board.new
535             begin
536               board.set_from_moves(moves_array)
537             rescue => err
538               # it will never happen since moves have already been checked
539               log_error "Failed to set up a buoy game: #{moves}"
540               return :continue
541             end
542             buoy.decrement_count(buoy_game)
543             Game::new(@player.game_name, @player, rival, board)
544           end
545         else
546           klass = Login.handicapped_game_name?(@game_name) || Board
547           board = klass.new
548           board.initial
549           Game::new(@player.game_name, @player, rival, board)
550         end
551       else # rival not found
552         if (@command_name == "GAME")
553           @player.status = "game_waiting"
554           @player.game_name = @game_name
555           if (@my_sente_str == "+")
556             @player.sente = true
557           elsif (@my_sente_str == "-")
558             @player.sente = false
559           else
560             @player.sente = nil
561           end
562         else                # challenge
563           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
564           @player.status = "connected"
565           @player.game_name = ""
566           @player.sente = nil
567         end
568       end
569       return :continue
570     end
571   end
572
573   # Command of CHAT
574   #
575   class ChatCommand < Command
576
577     # players array of [name, player]
578     #
579     def initialize(str, player, message, players)
580       super(str, player)
581       @message = message
582       @players = players
583     end
584
585     def call
586       @players.each do |name, p| # TODO player change name
587         if (p.protocol != LoginCSA::PROTOCOL)
588           p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message)) 
589         end
590       end
591       return :continue
592     end
593   end
594
595   # Command of LIST
596   #
597   class ListCommand < Command
598
599     # games array of [game_id, game]
600     #
601     def initialize(str, player, games)
602       super(str, player)
603       @games = games
604     end
605
606     def call
607       buf = Array::new
608       @games.each do |id, game|
609         buf.push(sprintf("##[LIST] %s\n", id))
610       end
611       buf.push("##[LIST] +OK\n")
612       @player.write_safe(buf.join)
613       return :continue
614     end
615   end
616
617   # Command of WHO
618   #
619   class WhoCommand < Command
620
621     # players array of [[name, player]]
622     #
623     def initialize(str, player, players)
624       super(str, player)
625       @players = players
626     end
627
628     def call
629       buf = Array::new
630       @players.each do |name, p|
631         buf.push(sprintf("##[WHO] %s\n", p.to_s))
632       end
633       buf.push("##[WHO] +OK\n")
634       @player.write_safe(buf.join)
635       return :continue
636     end
637   end
638
639   # Command of LOGOUT
640   #
641   class LogoutCommand < Command
642     def initialize(str, player)
643       super
644     end
645
646     def call
647       @player.status = "connected"
648       @player.write_safe("LOGOUT:completed\n")
649       return :return
650     end
651   end
652
653   # Command of CHALLENGE
654   #
655   class ChallengeCommand < Command
656     def initialize(str, player)
657       super
658     end
659
660     def call
661       # This command is only available for CSA's official testing server.
662       # So, this means nothing for this program.
663       @player.write_safe("CHALLENGE ACCEPTED\n")
664       return :continue
665     end
666   end
667
668   # Command for a space
669   #
670   class SpaceCommand < Command
671     def initialize(str, player)
672       super
673     end
674
675     def call
676       ## ignore null string
677       return :continue
678     end
679   end
680
681   # Command for an error
682   #
683   class ErrorCommand < Command
684     def initialize(str, player)
685       super
686     end
687
688     def call
689       msg = "##[ERROR] unknown command %s\n" % [@str]
690       @player.write_safe(msg)
691       log_error(msg)
692       return :continue
693     end
694   end
695
696   #
697   #
698   class SetBuoyCommand < Command
699
700     def initialize(str, player, game_name, moves, count)
701       super(str, player)
702       @game_name = game_name
703       @moves     = moves
704       @count     = count
705     end
706
707     def call
708       unless (Buoy.game_name?(@game_name))
709         @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
710         log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
711         return :continue
712       end
713       buoy = Buoy.new
714       unless buoy.is_new_game?(@game_name)
715         @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
716         log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
717         return :continue
718       end
719       if @count < 1
720         @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
721         log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
722         return :continue
723       end
724
725       # check moves
726       moves_array = Board::split_moves(@moves)
727       board = Board.new
728       begin
729         board.set_from_moves(moves_array)
730       rescue
731         raise WrongMoves
732       end
733
734       buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count)
735       buoy.add_game(buoy_game)
736       @player.write_safe(sprintf("##[SETBUOY] +OK\n"))
737       log_info("A buoy game was created: %s by %s" % [@game_name, @player.name])
738
739       # if two players, who are not @player, are waiting for a new game, start it
740       p1 = $league.get_player("game_waiting", @game_name, true, @player)
741       return :continue unless p1
742       p2 = $league.get_player("game_waiting", @game_name, false, @player)
743       return :continue unless p2
744
745       buoy.decrement_count(buoy_game)
746       game = Game::new(@game_name, p1, p2, board)
747       return :continue
748     rescue WrongMoves => e
749       @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
750       log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
751       return :continue
752     end
753   end
754
755   #
756   #
757   class DeleteBuoyCommand < Command
758     def initialize(str, player, game_name)
759       super(str, player)
760       @game_name = game_name
761     end
762
763     def call
764       buoy = Buoy.new
765       buoy_game = buoy.get_game(@game_name)
766       if buoy_game.instance_of?(NilBuoyGame)
767         @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
768         log_error "Game name not found: %s by %s" % [@game_name, @player.name]
769         return :continue
770       end
771
772       if buoy_game.owner != @player.name
773         @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
774         log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
775         return :continue
776       end
777
778       buoy.delete_game(buoy_game)
779       @player.write_safe(sprintf("##[DELETEBUOY] +OK\n"))
780       log_info("A buoy game was deleted: %s" % [@game_name])
781       return :continue
782     end
783   end
784
785   #
786   #
787   class GetBuoyCountCommand < Command
788     def initialize(str, player, game_name)
789       super(str, player)
790       @game_name = game_name
791     end
792
793     def call
794       buoy = Buoy.new
795       buoy_game = buoy.get_game(@game_name)
796       if buoy_game.instance_of?(NilBuoyGame)
797         @player.write_safe("##[GETBUOYCOUNT] -1\n")
798       else
799         @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
800       end
801       @player.write_safe("##[GETBUOYCOUNT] +OK\n")
802     end
803   end
804
805 end # module ShogiServer