OSDN Git Service

* [shogi-server]
[shogi-server/shogi-server.git] / shogi_server / command.rb
1 require 'kconv'
2 require 'shogi_server'
3
4 module ShogiServer
5
6   class Command
7     # Factory method
8     #
9     def Command.factory(str, player)
10       cmd = nil
11       case str 
12       when "" 
13         cmd = KeepAliveCommand.new(str, player)
14       when /^[\+\-][^%]/
15         cmd = MoveCommand.new(str, player)
16       when /^%[^%]/, :timeout
17         cmd = SpecialCommand.new(str, player)
18       when :exception
19         cmd = ExceptionCommand.new(str, player)
20       when /^REJECT/
21         cmd = RejectCommand.new(str, player)
22       when /^AGREE/
23         cmd = AgreeCommand.new(str, player)
24       when /^%%SHOW\s+(\S+)/
25         game_id = $1
26         cmd = ShowCommand.new(str, player, $league.games[game_id])
27       when /^%%MONITORON\s+(\S+)/
28         game_id = $1
29         cmd = MonitorOnCommand.new(str, player, $league.games[game_id])
30       when /^%%MONITOROFF\s+(\S+)/
31         game_id = $1
32         cmd = MonitorOffCommand.new(str, player, $league.games[game_id])
33       when /^%%HELP/
34         cmd = HelpCommand.new(str, player)
35       when /^%%RATING/
36         cmd = RatingCommand.new(str, player, $league.rated_players)
37       when /^%%VERSION/
38         cmd = VersionCommand.new(str, player)
39       when /^%%GAME\s*$/
40         cmd = GameCommand.new(str, player)
41       when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
42         command_name = $1
43         game_name = $2
44         my_sente_str = $3
45         cmd = GameChallengeCommand.new(str, player, 
46                                        command_name, game_name, my_sente_str)
47       when /^%%CHAT\s+(.+)/
48         message = $1
49         cmd = ChatCommand.new(str, player, message, $league.players)
50       when /^%%LIST/
51         cmd = ListCommand.new(str, player, $league.games)
52       when /^%%WHO/
53         cmd = WhoCommand.new(str, player, $league.players)
54       when /^LOGOUT/
55         cmd = LogoutCommand.new(str, player)
56       when /^CHALLENGE/
57         cmd = ChallengeCommand.new(str, player)
58       when /^\s*$/
59         cmd = SpaceCommand.new(str, player)
60       else
61         cmd = ErrorCommand.new(str, player)
62       end
63
64       return cmd
65     end
66
67     def initialize(str, player)
68       @str    = str
69       @player = player
70     end
71   end
72
73   # Application-level protocol for Keep-Alive.
74   # If the server receives an LF, it sends back an LF.  Note that the 30 sec
75   # rule (client may not send LF again within 30 sec) is not implemented
76   # yet.
77   #
78   class KeepAliveCommand < Command
79     def initialize(str, player)
80       super
81     end
82
83     def call
84       @player.write_safe("\n")
85       return :continue
86     end
87   end
88
89   # Command of moving a piece.
90   #
91   class MoveCommand < Command
92     def initialize(str, player)
93       super
94     end
95
96     def call
97       if (@player.status == "game")
98         array_str = @str.split(",")
99         move = array_str.shift
100         additional = array_str.shift
101         comment = nil
102         if /^'(.*)/ =~ additional
103           comment = array_str.unshift("'*#{$1.toeuc}")
104         end
105         s = @player.game.handle_one_move(move, @player)
106         @player.game.log_game(Kconv.toeuc(comment.first)) if (comment && comment.first && !s)
107         return :return if (s && @player.protocol == LoginCSA::PROTOCOL)
108       end
109       return :continue
110     end
111   end
112
113   # Command like "%TORYO" or :timeout
114   #
115   class SpecialCommand < Command
116     def initialize(str, player)
117       super
118     end
119
120     def call
121       rc = :continue
122       if (@player.status == "game")
123         rc = in_game_status()
124       elsif ["agree_waiting", "start_waiting"].include?(@player.status) 
125         rc = in_waiting_status()
126       else
127         log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].") unless @str == :timeout
128       end
129       return rc
130     end
131
132     def in_game_status
133       rc = :continue
134
135       s = @player.game.handle_one_move(@str, @player)
136       rc = :return if (s && @player.protocol == LoginCSA::PROTOCOL)
137
138       return rc
139     end
140
141     def in_waiting_status
142       rc = :continue
143
144       if @player.game.prepared_expire?
145         log_warning("#{@player.status} lasted too long. This play has been expired.")
146         @player.game.reject("the Server (timed out)")
147         rc = :return if (@player.protocol == LoginCSA::PROTOCOL)
148       end
149
150       return rc
151     end
152   end
153
154   # Command of :exception
155   #
156   class ExceptionCommand < Command
157     def initialize(str, player)
158       super
159     end
160
161     def call
162       log_error("Failed to receive a message from #{@player.name}.")
163       return :return
164     end
165   end
166
167   # Command of REJECT
168   #
169   class RejectCommand < Command
170     def initialize(str, player)
171       super
172     end
173
174     def call
175       if (@player.status == "agree_waiting")
176         @player.game.reject(@player.name)
177         return :return if (@player.protocol == LoginCSA::PROTOCOL)
178       else
179         log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
180         @player.write_safe(sprintf("##[ERROR] you are in %s status. REJECT is valid in agree_waiting status\n", @player.status))
181       end
182       return :continue
183     end
184   end
185
186   # Command of AGREE
187   #
188   class AgreeCommand < Command
189     def initialize(str, player)
190       super
191     end
192
193     def call
194       if (@player.status == "agree_waiting")
195         @player.status = "start_waiting"
196         if (@player.game.is_startable_status?)
197           @player.game.start
198         end
199       else
200         log_error("Received a command [#{@str}] from #{@player.name} in an inappropriate status [#{@player.status}].")
201         @player.write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @player.status))
202       end
203       return :continue
204     end
205   end
206
207   # Base Command calss requiring a game instance
208   #
209   class BaseCommandForGame < Command
210     def initialize(str, player, game)
211       super(str, player)
212       @game    = game
213       @game_id = game ? game.game_id : nil
214     end
215   end
216
217   # Command of SHOW
218   #
219   class ShowCommand < BaseCommandForGame
220     def initialize(str, player, game)
221       super
222     end
223
224     def call
225       if (@game)
226         @player.write_safe(@game.show.gsub(/^/, '##[SHOW] '))
227       end
228       @player.write_safe("##[SHOW] +OK\n")
229       return :continue
230     end
231   end
232
233   # Command of MONITORON
234   #
235   class MonitorOnCommand < BaseCommandForGame
236     def initialize(str, player, game)
237       super
238     end
239
240     def call
241       if (@game)
242         @game.monitoron(@player)
243         @player.write_safe(@game.show.gsub(/^/, "##[MONITOR][#{@game_id}] "))
244         @player.write_safe("##[MONITOR][#{@game_id}] +OK\n")
245       end
246       return :continue
247     end
248   end
249
250   # Command of MONITOROFF
251   #
252   class MonitorOffCommand < BaseCommandForGame
253     def initialize(str, player, game)
254       super
255     end
256
257     def call
258       if (@game)
259         @game.monitoroff(@player)
260       end
261       return :continue
262     end
263   end
264
265   # Command of HELP
266   #
267   class HelpCommand < Command
268     def initialize(str, player)
269       super
270     end
271
272     def call
273       @player.write_safe(
274         %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
275       return :continue
276     end
277   end
278
279   # Command of RATING
280   #
281   class RatingCommand < Command
282     def initialize(str, player, rated_players)
283       super(str, player)
284       @rated_players = rated_players
285     end
286
287     def call
288       @rated_players.sort {|a,b| b.rate <=> a.rate}.each do |p|
289         @player.write_safe("##[RATING] %s \t %4d @%s\n" % 
290                    [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
291       end
292       @player.write_safe("##[RATING] +OK\n")
293       return :continue
294     end
295   end
296
297   # Command of VERSION
298   #
299   class VersionCommand < Command
300     def initialize(str, player)
301       super
302     end
303
304     def call
305       @player.write_safe "##[VERSION] Shogi Server revision #{ShogiServer::Revision}\n"
306       @player.write_safe("##[VERSION] +OK\n")
307       return :continue
308     end
309   end
310
311   # Command of GAME
312   #
313   class GameCommand < Command
314     def initialize(str, player)
315       super
316     end
317
318     def call
319       if ((@player.status == "connected") || (@player.status == "game_waiting"))
320         @player.status = "connected"
321         @player.game_name = ""
322       else
323         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
324       end
325       return :continue
326     end
327   end
328
329   # Commando of game challenge
330   # TODO make a test case
331   #
332   class GameChallengeCommand < Command
333     def initialize(str, player, command_name, game_name, my_sente_str)
334       super(str, player)
335       @command_name = command_name
336       @game_name    = game_name
337       @my_sente_str = my_sente_str
338     end
339
340     def call
341       if (! Login::good_game_name?(@game_name))
342         @player.write_safe(sprintf("##[ERROR] bad game name\n"))
343         return :continue
344       elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
345         ## continue
346       else
347         @player.write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @player.status))
348         return :continue
349       end
350
351       rival = nil
352       if (League::Floodgate.game_name?(@game_name))
353         if (@my_sente_str != "*")
354           @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
355           return :continue
356         end
357         @player.sente = nil
358       else
359         if (@my_sente_str == "*")
360           rival = $league.get_player("game_waiting", @game_name, nil, @player) # no preference
361         elsif (@my_sente_str == "+")
362           rival = $league.get_player("game_waiting", @game_name, false, @player) # rival must be gote
363         elsif (@my_sente_str == "-")
364           rival = $league.get_player("game_waiting", @game_name, true, @player) # rival must be sente
365         else
366           ## never reached
367           @player.write_safe(sprintf("##[ERROR] bad game option\n"))
368           return :continue
369         end
370       end
371
372       if (rival)
373         @player.game_name = @game_name
374         if ((@my_sente_str == "*") && (rival.sente == nil))
375           if (rand(2) == 0)
376             @player.sente = true
377             rival.sente = false
378           else
379             @player.sente = false
380             rival.sente = true
381           end
382         elsif (rival.sente == true) # rival has higher priority
383           @player.sente = false
384         elsif (rival.sente == false)
385           @player.sente = true
386         elsif (@my_sente_str == "+")
387           @player.sente = true
388           rival.sente = false
389         elsif (@my_sente_str == "-")
390           @player.sente = false
391           rival.sente = true
392         else
393           ## never reached
394         end
395         Game::new(@player.game_name, @player, rival)
396       else # rival not found
397         if (@command_name == "GAME")
398           @player.status = "game_waiting"
399           @player.game_name = @game_name
400           if (@my_sente_str == "+")
401             @player.sente = true
402           elsif (@my_sente_str == "-")
403             @player.sente = false
404           else
405             @player.sente = nil
406           end
407         else                # challenge
408           @player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
409           @player.status = "connected"
410           @player.game_name = ""
411           @player.sente = nil
412         end
413       end
414       return :continue
415     end
416   end
417
418   # Command of CHAT
419   #
420   class ChatCommand < Command
421
422     # players array of [name, player]
423     #
424     def initialize(str, player, message, players)
425       super(str, player)
426       @message = message
427       @players = players
428     end
429
430     def call
431       @players.each do |name, p| # TODO player change name
432         if (p.protocol != LoginCSA::PROTOCOL)
433           p.write_safe(sprintf("##[CHAT][%s] %s\n", @player.name, @message)) 
434         end
435       end
436       return :continue
437     end
438   end
439
440   # Command of LIST
441   #
442   class ListCommand < Command
443
444     # games array of [game_id, game]
445     #
446     def initialize(str, player, games)
447       super(str, player)
448       @games = games
449     end
450
451     def call
452       buf = Array::new
453       @games.each do |id, game|
454         buf.push(sprintf("##[LIST] %s\n", id))
455       end
456       buf.push("##[LIST] +OK\n")
457       @player.write_safe(buf.join)
458       return :continue
459     end
460   end
461
462   # Command of WHO
463   #
464   class WhoCommand < Command
465
466     # players array of [[name, player]]
467     #
468     def initialize(str, player, players)
469       super(str, player)
470       @players = players
471     end
472
473     def call
474       buf = Array::new
475       @players.each do |name, p|
476         buf.push(sprintf("##[WHO] %s\n", p.to_s))
477       end
478       buf.push("##[WHO] +OK\n")
479       @player.write_safe(buf.join)
480       return :continue
481     end
482   end
483
484   # Command of LOGOUT
485   #
486   class LogoutCommand < Command
487     def initialize(str, player)
488       super
489     end
490
491     def call
492       @player.status = "connected"
493       @player.write_safe("LOGOUT:completed\n")
494       return :return
495     end
496   end
497
498   # Command of CHALLENGE
499   #
500   class ChallengeCommand < Command
501     def initialize(str, player)
502       super
503     end
504
505     def call
506       # This command is only available for CSA's official testing server.
507       # So, this means nothing for this program.
508       @player.write_safe("CHALLENGE ACCEPTED\n")
509       return :continue
510     end
511   end
512
513   # Command for a space
514   #
515   class SpaceCommand < Command
516     def initialize(str, player)
517       super
518     end
519
520     def call
521       ## ignore null string
522       return :continue
523     end
524   end
525
526   # Command for an error
527   #
528   class ErrorCommand < Command
529     def initialize(str, player)
530       super
531     end
532
533     def call
534       msg = "##[ERROR] unknown command %s\n" % [@str]
535       @player.write_safe(msg)
536       log_error(msg)
537       return :continue
538     end
539   end
540
541
542 end # module ShogiServer