OSDN Git Service

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