OSDN Git Service

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