OSDN Git Service

* The counter of a buoy game is decremented before the game is started.
[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             board = Board.new
448             begin
449               board.set_from_moves(buoy_game.moves)
450             rescue => err
451               # it will never happen since moves have already been checked
452               log_error "Failed to set up a buoy game: #{moves}"
453               return :continue
454             end
455             buoy.decrement_count(buoy_game)
456             Game::new(@player.game_name, @player, rival, board)
457           end
458         else
459           board = Board.new
460           board.initial
461           Game::new(@player.game_name, @player, rival, board)
462         end
463       else # rival not found
464         if (@command_name == "GAME")
465           @player.status = "game_waiting"
466           @player.game_name = @game_name
467           if (@my_sente_str == "+")
468             @player.sente = true
469           elsif (@my_sente_str == "-")
470             @player.sente = false
471           else
472             @player.sente = nil
473           end
474         else                # challenge
475           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
476           @player.status = "connected"
477           @player.game_name = ""
478           @player.sente = nil
479         end
480       end
481       return :continue
482     end
483   end
484
485   # Command of CHAT
486   #
487   class ChatCommand < Command
488
489     # players array of [name, player]
490     #
491     def initialize(str, player, message, players)
492       super(str, player)
493       @message = message
494       @players = players
495     end
496
497     def call
498       @players.each do |name, p| # TODO player change name
499         if (p.protocol != LoginCSA::PROTOCOL)
500           p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message)) 
501         end
502       end
503       return :continue
504     end
505   end
506
507   # Command of LIST
508   #
509   class ListCommand < Command
510
511     # games array of [game_id, game]
512     #
513     def initialize(str, player, games)
514       super(str, player)
515       @games = games
516     end
517
518     def call
519       buf = Array::new
520       @games.each do |id, game|
521         buf.push(sprintf("##[LIST] %s\n", id))
522       end
523       buf.push("##[LIST] +OK\n")
524       @player.write_safe(buf.join)
525       return :continue
526     end
527   end
528
529   # Command of WHO
530   #
531   class WhoCommand < Command
532
533     # players array of [[name, player]]
534     #
535     def initialize(str, player, players)
536       super(str, player)
537       @players = players
538     end
539
540     def call
541       buf = Array::new
542       @players.each do |name, p|
543         buf.push(sprintf("##[WHO] %s\n", p.to_s))
544       end
545       buf.push("##[WHO] +OK\n")
546       @player.write_safe(buf.join)
547       return :continue
548     end
549   end
550
551   # Command of LOGOUT
552   #
553   class LogoutCommand < Command
554     def initialize(str, player)
555       super
556     end
557
558     def call
559       @player.status = "connected"
560       @player.write_safe("LOGOUT:completed\n")
561       return :return
562     end
563   end
564
565   # Command of CHALLENGE
566   #
567   class ChallengeCommand < Command
568     def initialize(str, player)
569       super
570     end
571
572     def call
573       # This command is only available for CSA's official testing server.
574       # So, this means nothing for this program.
575       @player.write_safe("CHALLENGE ACCEPTED\n")
576       return :continue
577     end
578   end
579
580   # Command for a space
581   #
582   class SpaceCommand < Command
583     def initialize(str, player)
584       super
585     end
586
587     def call
588       ## ignore null string
589       return :continue
590     end
591   end
592
593   # Command for an error
594   #
595   class ErrorCommand < Command
596     def initialize(str, player)
597       super
598     end
599
600     def call
601       msg = "##[ERROR] unknown command %s\n" % [@str]
602       @player.write_safe(msg)
603       log_error(msg)
604       return :continue
605     end
606   end
607
608   #
609   #
610   class SetBuoyCommand < Command
611     class WrongMoves < ArgumentError; end
612
613     def initialize(str, player, game_name, moves, count)
614       super(str, player)
615       @game_name = game_name
616       @moves     = moves
617       @count     = count
618     end
619
620     def call
621       unless (Buoy.game_name?(@game_name))
622         @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
623         log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
624         return :continue
625       end
626       buoy = Buoy.new
627       unless buoy.is_new_game?(@game_name)
628         @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
629         log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
630         return :continue
631       end
632       if @count < 1
633         @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
634         log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
635         return :continue
636       end
637
638       # check moves
639       moves_array = split_moves @moves
640
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
666     private
667     
668     # Split a moves line into an array of a move string.
669     # If it fails to parse the moves, it raises WrongMoves.
670     # @param moves a moves line. Ex. "+776FU-3334Fu"
671     # @return an array of a move string. Ex. ["+7776FU", "-3334FU"]
672     #
673     def split_moves(moves)
674       ret = []
675
676       rs = moves.gsub %r{[\+\-]\d{4}\w{2}} do |s|
677              ret << s
678              ""
679            end
680       raise WrongMoves, rs unless rs.empty?
681
682       return ret
683     end
684   end
685
686   #
687   #
688   class DeleteBuoyCommand < Command
689     def initialize(str, player, game_name)
690       super(str, player)
691       @game_name = game_name
692     end
693
694     def call
695       buoy = Buoy.new
696       buoy_game = buoy.get_game(@game_name)
697       if buoy_game.instance_of?(NilBuoyGame)
698         @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
699         log_error "Game name not found: %s by %s" % [@game_name, @player.name]
700         return :continue
701       end
702
703       if buoy_game.owner != @player.name
704         @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
705         log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
706         return :continue
707       end
708
709       buoy.delete_game(buoy_game)
710       log_info("A buoy game was deleted: %s" % [@game_name])
711       return :continue
712     end
713   end
714
715   #
716   #
717   class GetBuoyCountCommand < Command
718     def initialize(str, player, game_name)
719       super(str, player)
720       @game_name = game_name
721     end
722
723     def call
724       buoy = Buoy.new
725       buoy_game = buoy.get_game(@game_name)
726       if buoy_game.instance_of?(NilBuoyGame)
727         @player.write_safe("##[GETBUOYCOUNT] -1\n")
728       else
729         @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
730       end
731       @player.write_safe("##[GETBUOYCOUNT] +OK\n")
732     end
733   end
734
735 end # module ShogiServer