OSDN Git Service

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