OSDN Git Service

75f6816252c2b739e54e7d23bd2e85e71c43eb94
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## $Id$
3
4 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
5 ## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
6 ##
7 ## This program is free software; you can redistribute it and/or modify
8 ## it under the terms of the GNU General Public License as published by
9 ## the Free Software Foundation; either version 2 of the License, or
10 ## (at your option) any later version.
11 ##
12 ## This program is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ## GNU General Public License for more details.
16 ##
17 ## You should have received a copy of the GNU General Public License
18 ## along with this program; if not, write to the Free Software
19 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 require 'getoptlong'
22 require 'thread'
23 require 'timeout'
24 require 'socket'
25 require 'yaml'
26 require 'yaml/store'
27 require 'digest/md5'
28 require 'webrick'
29 require 'fileutils'
30
31
32 class TCPSocket
33   def gets_timeout(t = Default_Timeout)
34     begin
35       timeout(t) do
36         return self.gets
37       end
38     rescue TimeoutError
39       return nil
40     rescue
41       return nil
42     end
43   end
44   def gets_safe(t = nil)
45     if (t && t > 0)
46       begin
47         timeout(t) do
48           return self.gets
49         end
50       rescue TimeoutError
51         return :timeout
52       rescue Exception => ex
53         log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
54         return :exception
55       end
56     else
57       begin
58         return self.gets
59       rescue
60         return nil
61       end
62     end
63   end
64   def write_safe(str)
65     begin
66       return self.write(str)
67     rescue
68       return nil
69     end
70   end
71 end
72
73
74 module ShogiServer # for a namespace
75
76 Max_Write_Queue_Size = 1000
77 Max_Identifier_Length = 32
78 Default_Timeout = 60            # for single socket operation
79
80 Default_Game_Name = "default-1500-0"
81
82 One_Time = 10
83 Least_Time_Per_Move = 1
84 Login_Time = 300                # time for LOGIN
85
86 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
87 Release.concat("-") if (Release == "")
88 Revision = "$Revision$".gsub(/[^\.\d]/, '')
89
90
91
92 class League
93
94   class Floodgate
95     class << self
96       def game_name?(str)
97         return "wdoor-900-0" == str
98       end
99     end
100
101     def initialize(league)
102       @league = league
103       @next_time = nil
104       charge
105     end
106
107     def run
108       @thread = Thread.new do
109         Thread.pass
110         while (true)
111           begin
112             sleep(20)
113             next if Time.now < @next_time
114             match_game
115             charge
116           rescue Exception => ex 
117             # ignore errors
118             log_error("[in Floodgate's thread] #{ex}")
119           end
120         end
121       end
122     end
123
124     def shutdown
125       @thread.kill if @thread
126     end
127
128     # private
129
130     def charge
131       now = Time.now
132       if now.min < 30
133         @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30)
134       else
135         @next_time = Time.mktime(now.year, now.month, now.day, now.hour+1)
136       end
137       # for test
138       # if now.sec < 30
139       #   @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
140       # else
141       #   @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min + 1)
142       # end
143     end
144
145     def match_game
146       players = @league.find_all_players do |pl|
147         pl.status == "game_waiting" &&
148         Floodgate.game_name?(pl.game_name) &&
149         pl.sente == nil
150       end
151       random_match(players)
152     end
153
154     def random_match(players)
155       if players.size < 2
156         log_message("Floodgate: too few players [%d]" % [players.size])
157         return
158       end
159       log_message("Floodgate: found %d players. Making games..." % [players.size])
160       random_players = players.sort{ rand < 0.5 ? 1 : -1 }
161       pairs = [[random_players.shift]]
162       while !random_players.empty? do
163         if pairs.last.size < 2
164           pairs.last << random_players.shift
165         else
166           pairs << [random_players.shift]
167         end 
168       end
169       if pairs.last.size < 2
170         pairs.pop
171       end
172       pairs.each do |pair|
173         start_game(pair.first, pair.last)
174       end
175     end
176
177     def start_game(p1, p2)
178       p1.sente = true
179       p2.sente = false
180       Game.new(p1.game_name, p1, p2)
181     end
182   end # class Floodgate
183
184   def initialize
185     @mutex = Mutex.new # guard @players
186     @games = Hash::new
187     @players = Hash::new
188     @event = nil
189     @dir = File.dirname(__FILE__)
190     @floodgate = Floodgate.new(self)
191     @floodgate.run
192   end
193   attr_accessor :players, :games, :event, :dir
194
195   def shutdown
196     @floodgate.shutdown
197   end
198
199   # this should be called just after instanciating a League object.
200   def setup_players_database
201     @db = YAML::Store.new(File.join(@dir, "players.yaml"))
202   end
203
204   def add(player)
205     self.load(player) if player.id
206     @mutex.synchronize do
207       @players[player.name] = player
208     end
209   end
210   
211   def delete(player)
212     @mutex.synchronize do
213       @players.delete(player.name)
214     end
215   end
216
217   def find_all_players
218     found = nil
219     @mutex.synchronize do
220       found = @players.find_all do |name, player|
221         yield player
222       end
223     end
224     return found.map {|a| a.last}
225   end
226   
227   def get_player(status, game_name, sente, searcher)
228     found = nil
229     @mutex.synchronize do
230       found = @players.find do |name, player|
231         (player.status == status) &&
232         (player.game_name == game_name) &&
233         ( (sente == nil) || 
234           (player.sente == nil) || 
235           (player.sente == sente) ) &&
236         (player.name != searcher.name)
237       end
238     end
239     return found ? found.last : nil
240   end
241   
242   def load(player)
243     hash = search(player.id)
244     if hash
245       # a current user
246       player.name         = hash['name']
247       player.rate         = hash['rate']
248       player.modified_at  = hash['last_modified']
249       player.rating_group = hash['rating_group']
250     end
251   end
252
253   def search(id)
254     hash = nil
255     @db.transaction do
256       break unless  @db["players"]
257       @db["players"].each do |group, players|
258         hash = players[id]
259         break if hash
260       end
261     end
262     hash
263   end
264
265   def rated_players
266     players = []
267     @db.transaction(true) do
268       break unless  @db["players"]
269       @db["players"].each do |group, players_hash|
270         players << players_hash.keys
271       end
272     end
273     return players.flatten.collect do |id|
274       p = BasicPlayer.new
275       p.id = id
276       self.load(p)
277       p
278     end
279   end
280
281   def floodgate_game?(game_name)
282     return "wdoor-0-10" == game_name
283   end
284 end
285
286
287 ######################################################
288 # Processes the LOGIN command.
289 #
290 class Login
291   def Login.good_login?(str)
292     tokens = str.split
293     if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
294         (tokens[0] == "LOGIN") &&
295         (good_identifier?(tokens[1])))
296       return true
297     else
298       return false
299     end
300   end
301
302   def Login.good_game_name?(str)
303     if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1)))
304       return true
305     else
306       return false
307     end
308   end
309
310   def Login.good_identifier?(str)
311     if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
312       return true
313     else
314       return false
315     end
316   end
317
318   def Login.factory(str, player)
319     (login, player.name, password, ext) = str.chomp.split
320     if (ext)
321       return Loginx1.new(player, password)
322     else
323       return LoginCSA.new(player, password)
324     end
325   end
326
327   attr_reader :player
328   
329   # the first command that will be executed just after LOGIN.
330   # If it is nil, the default process will be started.
331   attr_reader :csa_1st_str
332
333   def initialize(player, password)
334     @player = player
335     @csa_1st_str = nil
336     parse_password(password)
337   end
338
339   def process
340     @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name))
341     log_message(sprintf("user %s run in %s mode", @player.name, @player.protocol))
342   end
343
344   def incorrect_duplicated_player(str)
345     @player.write_safe("LOGIN:incorrect\n")
346     @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4)
347     sleep 3 # wait for sending the above messages.
348     @player.name = "%s [duplicated]" % [@player.name]
349     @player.finish
350   end
351 end
352
353 ######################################################
354 # Processes LOGIN for the CSA standard mode.
355 #
356 class LoginCSA < Login
357   PROTOCOL = "CSA"
358
359   def initialize(player, password)
360     @gamename = nil
361     super
362     @player.protocol = PROTOCOL
363   end
364
365   def parse_password(password)
366     if Login.good_game_name?(password)
367       @gamename = password
368       @player.set_password(nil)
369     elsif password.split(",").size > 1
370       @gamename, *trip = password.split(",")
371       @player.set_password(trip.join(","))
372     else
373       @player.set_password(password)
374       @gamename = Default_Game_Name
375     end
376     @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name
377   end
378
379   def process
380     super
381     @csa_1st_str = "%%GAME #{@gamename} *"
382   end
383 end
384
385 ######################################################
386 # Processes LOGIN for the extented mode.
387 #
388 class Loginx1 < Login
389   PROTOCOL = "x1"
390
391   def initialize(player, password)
392     super
393     @player.protocol = PROTOCOL
394   end
395   
396   def parse_password(password)
397     @player.set_password(password)
398   end
399
400   def process
401     super
402     @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL))
403   end
404 end
405
406
407 class BasicPlayer
408   # Idetifier of the player in the rating system
409   attr_accessor :id
410
411   # Name of the player
412   attr_accessor :name
413   
414   # Password of the player, which does not include a trip
415   attr_accessor :password
416
417   # Score in the rating sysem
418   attr_accessor :rate
419   
420   # Group in the rating system
421   attr_accessor :rating_group
422
423   # Last timestamp when the rate was modified
424   attr_accessor :modified_at
425
426   def initialize
427     @name = nil
428     @password = nil
429   end
430
431   def modified_at
432     @modified_at || Time.now
433   end
434
435   def rate=(new_rate)
436     if @rate != new_rate
437       @rate = new_rate
438       @modified_at = Time.now
439     end
440   end
441
442   def rated?
443     @id != nil
444   end
445
446   def simple_id
447     if @trip
448       simple_name = @name.gsub(/@.*?$/, '')
449       "%s+%s" % [simple_name, @trip[0..8]]
450     else
451       @name
452     end
453   end
454
455   ##
456   # Parses str in the LOGIN command, sets up @id and @trip
457   #
458   def set_password(str)
459     if str && !str.empty?
460       @password = str.strip
461       @id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
462     else
463       @id = @password = nil
464     end
465   end
466 end
467
468
469 class Player < BasicPlayer
470   def initialize(str, socket)
471     super()
472     @socket = socket
473     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
474
475     @protocol = nil             # CSA or x1
476     @eol = "\m"                 # favorite eol code
477     @game = nil
478     @game_name = ""
479     @mytime = 0                 # set in start method also
480     @sente = nil
481     @write_queue = Queue::new
482     @main_thread = Thread::current
483     @writer_thread = Thread::start do
484       Thread.pass
485       writer()
486     end
487   end
488
489   attr_accessor :socket, :status
490   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
491   attr_accessor :main_thread, :writer_thread, :write_queue
492   
493   def kill
494     log_message(sprintf("user %s killed", @name))
495     if (@game)
496       @game.kill(self)
497     end
498     finish
499     Thread::kill(@main_thread) if @main_thread
500   end
501
502   def finish
503     if (@status != "finished")
504       @status = "finished"
505       log_message(sprintf("user %s finish", @name))    
506       # TODO you should confirm that there is no message in the queue.
507       Thread::kill(@writer_thread) if @writer_thread
508       begin
509 #        @socket.close if (! @socket.closed?)
510       rescue
511         log_message(sprintf("user %s finish failed", @name))    
512       end
513     end
514   end
515
516   def write_safe(str)
517     @write_queue.push(str.gsub(/[\r\n]+/, @eol))
518   end
519
520   def writer
521     while (str = @write_queue.pop)
522       begin
523         @socket.write(str)
524       rescue Exception => ex
525         log_error("Failed to send a message to #{@name}.")
526         log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
527         return
528       end
529     end
530   end
531
532   def to_s
533     if ((status == "game_waiting") ||
534         (status == "start_waiting") ||
535         (status == "agree_waiting") ||
536         (status == "game"))
537       if (@sente)
538         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
539       elsif (@sente == false)
540         return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
541       elsif (@sente == nil)
542         return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
543       end
544     else
545       return sprintf("%s %s %s", @name, @protocol, @status)
546     end
547   end
548
549   def run(csa_1st_str=nil)
550     while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
551       begin
552         $mutex.lock
553
554         if (@writer_thread == nil || @writer_thread.status == false)
555           # The writer_thread has been killed because of socket errors.
556           return
557         end
558
559         if (csa_1st_str)
560           str = csa_1st_str
561           csa_1st_str = nil
562         end
563         if (@write_queue.size > Max_Write_Queue_Size)
564           log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
565                 return
566         end
567
568         if (@status == "finished")
569           return
570         end
571         str.chomp! if (str.class == String) # may be strip! ?
572         log_message(str) if $DEBUG
573         case str 
574         when "" 
575           # Application-level protocol for Keep-Alive
576           # If the server gets LF, it sends back LF.
577           # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
578           write_safe("\n")
579         when /^[\+\-][^%]/
580           if (@status == "game")
581             array_str = str.split(",")
582             move = array_str.shift
583             additional = array_str.shift
584             if /^'(.*)/ =~ additional
585               comment = array_str.unshift("'*#{$1}")
586             end
587             s = @game.handle_one_move(move, self)
588             @game.fh.print("#{comment}\n") if (comment && !s)
589             return if (s && @protocol == LoginCSA::PROTOCOL)
590           end
591         when /^%[^%]/, :timeout
592           if (@status == "game")
593             s = @game.handle_one_move(str, self)
594             return if (s && @protocol == LoginCSA::PROTOCOL)
595           # else
596           #   begin
597           #     @socket.write("##[KEEPALIVE] #{Time.now}\n")
598           #   rescue Exception => ex
599           #     log_error("Failed to send a keepalive to #{@name}.")
600           #     log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
601           #     return
602           #   end
603           end
604         when :exception
605           log_error("Failed to receive a message from #{@name}.")
606           return
607         when /^REJECT/
608           if (@status == "agree_waiting")
609             @game.reject(@name)
610             return if (@protocol == LoginCSA::PROTOCOL)
611           else
612             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
613           end
614         when /^AGREE/
615           if (@status == "agree_waiting")
616             @status = "start_waiting"
617             if ((@game.sente.status == "start_waiting") &&
618                 (@game.gote.status == "start_waiting"))
619               @game.start
620               @game.sente.status = "game"
621               @game.gote.status = "game"
622             end
623           else
624             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
625           end
626         when /^%%SHOW\s+(\S+)/
627           game_id = $1
628           if (LEAGUE.games[game_id])
629             write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
630           end
631           write_safe("##[SHOW] +OK\n")
632         when /^%%MONITORON\s+(\S+)/
633           game_id = $1
634           if (LEAGUE.games[game_id])
635             LEAGUE.games[game_id].monitoron(self)
636             write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
637             write_safe("##[MONITOR][#{game_id}] +OK\n")
638           end
639         when /^%%MONITOROFF\s+(\S+)/
640           game_id = $1
641           if (LEAGUE.games[game_id])
642             LEAGUE.games[game_id].monitoroff(self)
643           end
644         when /^%%HELP/
645           write_safe(
646             %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
647         when /^%%RATING/
648           players = LEAGUE.rated_players
649           players.sort {|a,b| b.rate <=> a.rate}.each do |p|
650             write_safe("##[RATING] %s \t %4d @%s\n" % 
651                        [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
652           end
653           write_safe("##[RATING] +OK\n")
654         when /^%%VERSION/
655           write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
656           write_safe("##[VERSION] +OK\n")
657         when /^%%GAME\s*$/
658           if ((@status == "connected") || (@status == "game_waiting"))
659             @status = "connected"
660             @game_name = ""
661           else
662             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
663           end
664         when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
665           command_name = $1
666           game_name = $2
667           my_sente_str = $3
668           if (! Login::good_game_name?(game_name))
669             write_safe(sprintf("##[ERROR] bad game name\n"))
670             next
671           elsif ((@status == "connected") || (@status == "game_waiting"))
672             ## continue
673           else
674             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
675             next
676           end
677
678           rival = nil
679           if (League::Floodgate.game_name?(game_name))
680             if (my_sente_str != "*")
681               write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n"), my_sente_str, game_name)
682               next
683             end
684             @sente = nil
685           else
686             if (my_sente_str == "*")
687               rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
688             elsif (my_sente_str == "+")
689               rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
690             elsif (my_sente_str == "-")
691               rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
692             else
693               ## never reached
694               write_safe(sprintf("##[ERROR] bad game option\n"))
695               next
696             end
697           end
698
699           if (rival)
700             @game_name = game_name
701             if ((my_sente_str == "*") && (rival.sente == nil))
702               if (rand(2) == 0)
703                 @sente = true
704                 rival.sente = false
705               else
706                 @sente = false
707                 rival.sente = true
708               end
709             elsif (rival.sente == true) # rival has higher priority
710               @sente = false
711             elsif (rival.sente == false)
712               @sente = true
713             elsif (my_sente_str == "+")
714               @sente = true
715               rival.sente = false
716             elsif (my_sente_str == "-")
717               @sente = false
718               rival.sente = true
719             else
720               ## never reached
721             end
722             Game::new(@game_name, self, rival)
723           else # rival not found
724             if (command_name == "GAME")
725               @status = "game_waiting"
726               @game_name = game_name
727               if (my_sente_str == "+")
728                 @sente = true
729               elsif (my_sente_str == "-")
730                 @sente = false
731               else
732                 @sente = nil
733               end
734             else                # challenge
735               write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
736               @status = "connected"
737               @game_name = ""
738               @sente = nil
739             end
740           end
741         when /^%%CHAT\s+(.+)/
742           message = $1
743           LEAGUE.players.each do |name, player|
744             if (player.protocol != LoginCSA::PROTOCOL)
745               player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
746             end
747           end
748         when /^%%LIST/
749           buf = Array::new
750           LEAGUE.games.each do |id, game|
751             buf.push(sprintf("##[LIST] %s\n", id))
752           end
753           buf.push("##[LIST] +OK\n")
754           write_safe(buf.join)
755         when /^%%WHO/
756           buf = Array::new
757           LEAGUE.players.each do |name, player|
758             buf.push(sprintf("##[WHO] %s\n", player.to_s))
759           end
760           buf.push("##[WHO] +OK\n")
761           write_safe(buf.join)
762         when /^LOGOUT/
763           @status = "connected"
764           write_safe("LOGOUT:completed\n")
765           return
766         when /^CHALLENGE/
767           # This command is only available for CSA's official testing server.
768           # So, this means nothing for this program.
769           write_safe("CHALLENGE ACCEPTED\n")
770         when /^\s*$/
771           ## ignore null string
772         else
773           msg = "##[ERROR] unknown command %s\n" % [str]
774           write_safe(msg)
775           log_error(msg)
776         end
777       ensure
778         $mutex.unlock
779       end
780     end # enf of while
781   end # def run
782 end # class
783
784 class Piece
785   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
786   def initialize(board, x, y, sente, promoted=false)
787     @board = board
788     @x = x
789     @y = y
790     @sente = sente
791     @promoted = promoted
792
793     if ((x == 0) || (y == 0))
794       if (sente)
795         hands = board.sente_hands
796       else
797         hands = board.gote_hands
798       end
799       hands.push(self)
800       hands.sort! {|a, b|
801         a.name <=> b.name
802       }
803     else
804       @board.array[x][y] = self
805     end
806   end
807   attr_accessor :promoted, :sente, :x, :y, :board
808
809   def room_of_head?(x, y, name)
810     true
811   end
812
813   def movable_grids
814     return adjacent_movable_grids + far_movable_grids
815   end
816
817   def far_movable_grids
818     return []
819   end
820
821   def jump_to?(x, y)
822     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
823       if ((@board.array[x][y] == nil) || # dst is empty
824           (@board.array[x][y].sente != @sente)) # dst is enemy
825         return true
826       end
827     end
828     return false
829   end
830
831   def put_to?(x, y)
832     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
833       if (@board.array[x][y] == nil) # dst is empty?
834         return true
835       end
836     end
837     return false
838   end
839
840   def adjacent_movable_grids
841     grids = Array::new
842     if (@promoted)
843       moves = @promoted_moves
844     else
845       moves = @normal_moves
846     end
847     moves.each do |(dx, dy)|
848       if (@sente)
849         cand_y = @y - dy
850       else
851         cand_y = @y + dy
852       end
853       cand_x = @x + dx
854       if (jump_to?(cand_x, cand_y))
855         grids.push([cand_x, cand_y])
856       end
857     end
858     return grids
859   end
860
861   def move_to?(x, y, name)
862     return false if (! room_of_head?(x, y, name))
863     return false if ((name != @name) && (name != @promoted_name))
864     return false if (@promoted && (name != @promoted_name)) # can't un-promote
865
866     if (! @promoted)
867       return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
868       if (@sente)
869         return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
870       else
871         return false if ((6 >= @y) && (6 >= y) && (name != @name))
872       end
873     end
874
875     if ((@x == 0) || (@y == 0))
876       return jump_to?(x, y)
877     else
878       return movable_grids.include?([x, y])
879     end
880   end
881
882   def move_to(x, y)
883     if ((@x == 0) || (@y == 0))
884       if (@sente)
885         @board.sente_hands.delete(self)
886       else
887         @board.gote_hands.delete(self)
888       end
889       @board.array[x][y] = self
890     elsif ((x == 0) || (y == 0))
891       @promoted = false         # clear promoted flag before moving to hands
892       if (@sente)
893         @board.sente_hands.push(self)
894       else
895         @board.gote_hands.push(self)
896       end
897       @board.array[@x][@y] = nil
898     else
899       @board.array[@x][@y] = nil
900       @board.array[x][y] = self
901     end
902     @x = x
903     @y = y
904   end
905
906   def point
907     @point
908   end
909
910   def name
911     @name
912   end
913
914   def promoted_name
915     @promoted_name
916   end
917
918   def to_s
919     if (@sente)
920       sg = "+"
921     else
922       sg = "-"
923     end
924     if (@promoted)
925       n = @promoted_name
926     else
927       n = @name
928     end
929     return sg + n
930   end
931 end
932
933 class PieceFU < Piece
934   def initialize(*arg)
935     @point = 1
936     @normal_moves = [[0, +1]]
937     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
938     @name = "FU"
939     @promoted_name = "TO"
940     super
941   end
942   def room_of_head?(x, y, name)
943     if (name == "FU")
944       if (@sente)
945         return false if (y == 1)
946       else
947         return false if (y == 9)
948       end
949       ## 2fu check
950       c = 0
951       iy = 1
952       while (iy <= 9)
953         if ((iy  != @y) &&      # not source position
954             @board.array[x][iy] &&
955             (@board.array[x][iy].sente == @sente) && # mine
956             (@board.array[x][iy].name == "FU") &&
957             (@board.array[x][iy].promoted == false))
958           return false
959         end
960         iy = iy + 1
961       end
962     end
963     return true
964   end
965 end
966
967 class PieceKY  < Piece
968   def initialize(*arg)
969     @point = 1
970     @normal_moves = []
971     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
972     @name = "KY"
973     @promoted_name = "NY"
974     super
975   end
976   def room_of_head?(x, y, name)
977     if (name == "KY")
978       if (@sente)
979         return false if (y == 1)
980       else
981         return false if (y == 9)
982       end
983     end
984     return true
985   end
986   def far_movable_grids
987     grids = Array::new
988     if (@promoted)
989       return []
990     else
991       if (@sente)                 # up
992         cand_x = @x
993         cand_y = @y - 1
994         while (jump_to?(cand_x, cand_y))
995           grids.push([cand_x, cand_y])
996           break if (! put_to?(cand_x, cand_y))
997           cand_y = cand_y - 1
998         end
999       else                        # down
1000         cand_x = @x
1001         cand_y = @y + 1
1002         while (jump_to?(cand_x, cand_y))
1003           grids.push([cand_x, cand_y])
1004           break if (! put_to?(cand_x, cand_y))
1005           cand_y = cand_y + 1
1006         end
1007       end
1008       return grids
1009     end
1010   end
1011 end
1012 class PieceKE  < Piece
1013   def initialize(*arg)
1014     @point = 1
1015     @normal_moves = [[+1, +2], [-1, +2]]
1016     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
1017     @name = "KE"
1018     @promoted_name = "NK"
1019     super
1020   end
1021   def room_of_head?(x, y, name)
1022     if (name == "KE")
1023       if (@sente)
1024         return false if ((y == 1) || (y == 2))
1025       else
1026         return false if ((y == 9) || (y == 8))
1027       end
1028     end
1029     return true
1030   end
1031 end
1032 class PieceGI  < Piece
1033   def initialize(*arg)
1034     @point = 1
1035     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
1036     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
1037     @name = "GI"
1038     @promoted_name = "NG"
1039     super
1040   end
1041 end
1042 class PieceKI  < Piece
1043   def initialize(*arg)
1044     @point = 1
1045     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
1046     @promoted_moves = []
1047     @name = "KI"
1048     @promoted_name = nil
1049     super
1050   end
1051 end
1052 class PieceKA  < Piece
1053   def initialize(*arg)
1054     @point = 5
1055     @normal_moves = []
1056     @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
1057     @name = "KA"
1058     @promoted_name = "UM"
1059     super
1060   end
1061   def far_movable_grids
1062     grids = Array::new
1063     ## up right
1064     cand_x = @x - 1
1065     cand_y = @y - 1
1066     while (jump_to?(cand_x, cand_y))
1067       grids.push([cand_x, cand_y])
1068       break if (! put_to?(cand_x, cand_y))
1069       cand_x = cand_x - 1
1070       cand_y = cand_y - 1
1071     end
1072     ## down right
1073     cand_x = @x - 1
1074     cand_y = @y + 1
1075     while (jump_to?(cand_x, cand_y))
1076       grids.push([cand_x, cand_y])
1077       break if (! put_to?(cand_x, cand_y))
1078       cand_x = cand_x - 1
1079       cand_y = cand_y + 1
1080     end
1081     ## up left
1082     cand_x = @x + 1
1083     cand_y = @y - 1
1084     while (jump_to?(cand_x, cand_y))
1085       grids.push([cand_x, cand_y])
1086       break if (! put_to?(cand_x, cand_y))
1087       cand_x = cand_x + 1
1088       cand_y = cand_y - 1
1089     end
1090     ## down left
1091     cand_x = @x + 1
1092     cand_y = @y + 1
1093     while (jump_to?(cand_x, cand_y))
1094       grids.push([cand_x, cand_y])
1095       break if (! put_to?(cand_x, cand_y))
1096       cand_x = cand_x + 1
1097       cand_y = cand_y + 1
1098     end
1099     return grids
1100   end
1101 end
1102 class PieceHI  < Piece
1103   def initialize(*arg)
1104     @point = 5
1105     @normal_moves = []
1106     @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
1107     @name = "HI"
1108     @promoted_name = "RY"
1109     super
1110   end
1111   def far_movable_grids
1112     grids = Array::new
1113     ## up
1114     cand_x = @x
1115     cand_y = @y - 1
1116     while (jump_to?(cand_x, cand_y))
1117       grids.push([cand_x, cand_y])
1118       break if (! put_to?(cand_x, cand_y))
1119       cand_y = cand_y - 1
1120     end
1121     ## down
1122     cand_x = @x
1123     cand_y = @y + 1
1124     while (jump_to?(cand_x, cand_y))
1125       grids.push([cand_x, cand_y])
1126       break if (! put_to?(cand_x, cand_y))
1127       cand_y = cand_y + 1
1128     end
1129     ## right
1130     cand_x = @x - 1
1131     cand_y = @y
1132     while (jump_to?(cand_x, cand_y))
1133       grids.push([cand_x, cand_y])
1134       break if (! put_to?(cand_x, cand_y))
1135       cand_x = cand_x - 1
1136     end
1137     ## down
1138     cand_x = @x + 1
1139     cand_y = @y
1140     while (jump_to?(cand_x, cand_y))
1141       grids.push([cand_x, cand_y])
1142       break if (! put_to?(cand_x, cand_y))
1143       cand_x = cand_x + 1
1144     end
1145     return grids
1146   end
1147 end
1148 class PieceOU < Piece
1149   def initialize(*arg)
1150     @point = 0
1151     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
1152     @promoted_moves = []
1153     @name = "OU"
1154     @promoted_name = nil
1155     super
1156   end
1157 end
1158
1159 class Board
1160   def initialize
1161     @sente_hands = Array::new
1162     @gote_hands  = Array::new
1163     @history       = Hash::new(0)
1164     @sente_history = Hash::new(0)
1165     @gote_history  = Hash::new(0)
1166     @array = [[], [], [], [], [], [], [], [], [], []]
1167     @move_count = 0
1168   end
1169   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
1170   attr_reader :move_count
1171
1172   def initial
1173     PieceKY::new(self, 1, 1, false)
1174     PieceKE::new(self, 2, 1, false)
1175     PieceGI::new(self, 3, 1, false)
1176     PieceKI::new(self, 4, 1, false)
1177     PieceOU::new(self, 5, 1, false)
1178     PieceKI::new(self, 6, 1, false)
1179     PieceGI::new(self, 7, 1, false)
1180     PieceKE::new(self, 8, 1, false)
1181     PieceKY::new(self, 9, 1, false)
1182     PieceKA::new(self, 2, 2, false)
1183     PieceHI::new(self, 8, 2, false)
1184     (1..9).each do |i|
1185       PieceFU::new(self, i, 3, false)
1186     end
1187
1188     PieceKY::new(self, 1, 9, true)
1189     PieceKE::new(self, 2, 9, true)
1190     PieceGI::new(self, 3, 9, true)
1191     PieceKI::new(self, 4, 9, true)
1192     PieceOU::new(self, 5, 9, true)
1193     PieceKI::new(self, 6, 9, true)
1194     PieceGI::new(self, 7, 9, true)
1195     PieceKE::new(self, 8, 9, true)
1196     PieceKY::new(self, 9, 9, true)
1197     PieceKA::new(self, 8, 8, true)
1198     PieceHI::new(self, 2, 8, true)
1199     (1..9).each do |i|
1200       PieceFU::new(self, i, 7, true)
1201     end
1202   end
1203
1204   def have_piece?(hands, name)
1205     piece = hands.find { |i|
1206       i.name == name
1207     }
1208     return piece
1209   end
1210
1211   def move_to(x0, y0, x1, y1, name, sente)
1212     if (sente)
1213       hands = @sente_hands
1214     else
1215       hands = @gote_hands
1216     end
1217
1218     if ((x0 == 0) || (y0 == 0))
1219       piece = have_piece?(hands, name)
1220       return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece?
1221       piece.move_to(x1, y1)
1222     else
1223       return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))  # TODO null check?
1224       if (@array[x0][y0].name != name) # promoted ?
1225         @array[x0][y0].promoted = true
1226       end
1227       if (@array[x1][y1]) # capture
1228         if (@array[x1][y1].name == "OU")
1229           return :outori        # return board update
1230         end
1231         @array[x1][y1].sente = @array[x0][y0].sente
1232         @array[x1][y1].move_to(0, 0)
1233         hands.sort! {|a, b| # TODO refactor. Move to Piece class
1234           a.name <=> b.name
1235         }
1236       end
1237       @array[x0][y0].move_to(x1, y1)
1238     end
1239     @move_count += 1
1240     return true
1241   end
1242
1243   def look_for_ou(sente)
1244     x = 1
1245     while (x <= 9)
1246       y = 1
1247       while (y <= 9)
1248         if (@array[x][y] &&
1249             (@array[x][y].name == "OU") &&
1250             (@array[x][y].sente == sente))
1251           return @array[x][y]
1252         end
1253         y = y + 1
1254       end
1255       x = x + 1
1256     end
1257     raise "can't find ou"
1258   end
1259
1260   # note checkmate, but check. sente is checked.
1261   def checkmated?(sente)        # sente is loosing
1262     ou = look_for_ou(sente)
1263     x = 1
1264     while (x <= 9)
1265       y = 1
1266       while (y <= 9)
1267         if (@array[x][y] &&
1268             (@array[x][y].sente != sente))
1269           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1270             return true
1271           end
1272         end
1273         y = y + 1
1274       end
1275       x = x + 1
1276     end
1277     return false
1278   end
1279
1280   def uchifuzume?(sente)
1281     rival_ou = look_for_ou(! sente)   # rival's ou
1282     if (sente)                  # rival is gote
1283       if ((rival_ou.y != 9) &&
1284           (@array[rival_ou.x][rival_ou.y + 1]) &&
1285           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1286           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1287         fu_x = rival_ou.x
1288         fu_y = rival_ou.y + 1
1289       else
1290         return false
1291       end
1292     else                        # gote
1293       if ((rival_ou.y != 0) &&
1294           (@array[rival_ou.x][rival_ou.y - 1]) &&
1295           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1296           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1297         fu_x = rival_ou.x
1298         fu_y = rival_ou.y - 1
1299       else
1300         return false
1301       end
1302     end
1303     
1304     ## case: rival_ou is moving
1305     escaped = false
1306     rival_ou.movable_grids.each do |(cand_x, cand_y)|
1307       tmp_board = Marshal.load(Marshal.dump(self))
1308       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1309       raise "internal error" if (s != true)
1310       if (! tmp_board.checkmated?(! sente)) # good move
1311         return false
1312       end
1313     end
1314
1315     ## case: rival is capturing fu
1316     x = 1
1317     while (x <= 9)
1318       y = 1
1319       while (y <= 9)
1320         if (@array[x][y] &&
1321             (@array[x][y].sente != sente) &&
1322             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1323           if (@array[x][y].promoted)
1324             name = @array[x][y].promoted_name
1325           else
1326             name = @array[x][y].name
1327           end
1328           tmp_board = Marshal.load(Marshal.dump(self))
1329           s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1330           raise "internal error" if (s != true)
1331           if (! tmp_board.checkmated?(! sente)) # good move
1332             return false
1333           end
1334         end
1335         y = y + 1
1336       end
1337       x = x + 1
1338     end
1339     return true
1340   end
1341
1342   # @[sente|gote]_history has at least one item while the player is checking the other or 
1343   # the other escapes.
1344   def update_sennichite(player)
1345     str = to_s
1346     @history[str] += 1
1347     if checkmated?(!player)
1348       if (player)
1349         @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
1350       else
1351         @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
1352       end
1353     else
1354       if (player)
1355         @sente_history.clear # no more continuous check
1356       else
1357         @gote_history.clear  # no more continuous check
1358       end
1359     end
1360     if @sente_history.size > 0  # possible for Sente's or Gote's turn
1361       @sente_history[str] += 1
1362     end
1363     if @gote_history.size > 0   # possible for Sente's or Gote's turn
1364       @gote_history[str] += 1
1365     end
1366   end
1367
1368   def oute_sennichite?(player)
1369     if (@sente_history[to_s] >= 4)
1370       return :oute_sennichite_sente_lose
1371     elsif (@gote_history[to_s] >= 4)
1372       return :oute_sennichite_gote_lose
1373     else
1374       return nil
1375     end
1376   end
1377
1378   def sennichite?(sente)
1379     if (@history[to_s] >= 4) # already 3 times
1380       return true
1381     end
1382     return false
1383   end
1384
1385   def good_kachi?(sente)
1386     if (checkmated?(sente))
1387       puts "'NG: Checkmating." if $DEBUG
1388       return false 
1389     end
1390     
1391     ou = look_for_ou(sente)
1392     if (sente && (ou.y >= 4))
1393       puts "'NG: Black's OU does not enter yet." if $DEBUG
1394       return false     
1395     end  
1396     if (! sente && (ou.y <= 6))
1397       puts "'NG: White's OU does not enter yet." if $DEBUG
1398       return false 
1399     end
1400       
1401     number = 0
1402     point = 0
1403
1404     if (sente)
1405       hands = @sente_hands
1406       r = [1, 2, 3]
1407     else
1408       hands = @gote_hands
1409       r = [7, 8, 9]
1410     end
1411     r.each do |y|
1412       x = 1
1413       while (x <= 9)
1414         if (@array[x][y] &&
1415             (@array[x][y].sente == sente) &&
1416             (@array[x][y].point > 0))
1417           point = point + @array[x][y].point
1418           number = number + 1
1419         end
1420         x = x + 1
1421       end
1422     end
1423     hands.each do |piece|
1424       point = point + piece.point
1425     end
1426
1427     if (number < 10)
1428       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1429       return false     
1430     end  
1431     if (sente)
1432       if (point < 28)
1433         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1434         return false 
1435       end  
1436     else
1437       if (point < 27)
1438         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1439         return false 
1440       end
1441     end
1442
1443     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1444     return true
1445   end
1446
1447   # sente is nil only if tests in test_board run
1448   def handle_one_move(str, sente=nil)
1449     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1450       sg = $1
1451       x0 = $2.to_i
1452       y0 = $3.to_i
1453       x1 = $4.to_i
1454       y1 = $5.to_i
1455       name = $6
1456     elsif (str =~ /^%KACHI/)
1457       raise ArgumentError, "sente is null", caller if sente == nil
1458       if (good_kachi?(sente))
1459         return :kachi_win
1460       else
1461         return :kachi_lose
1462       end
1463     elsif (str =~ /^%TORYO/)
1464       return :toryo
1465     else
1466       return :illegal
1467     end
1468     
1469     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1470         ((x0 != 0) || (y0 != 0)))
1471       return :illegal
1472     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1473       return :illegal
1474     end
1475     
1476
1477     if (sg == "+")
1478       sente = true if sente == nil           # deprecated
1479       return :illegal unless sente == true   # black player's move must be black
1480       hands = @sente_hands
1481     else
1482       sente = false if sente == nil          # deprecated
1483       return :illegal unless sente == false  # white player's move must be white
1484       hands = @gote_hands
1485     end
1486     
1487     ## source check
1488     if ((x0 == 0) && (y0 == 0))
1489       return :illegal if (! have_piece?(hands, name))
1490     elsif (! @array[x0][y0])
1491       return :illegal           # no piece
1492     elsif (@array[x0][y0].sente != sente)
1493       return :illegal           # this is not mine
1494     elsif (@array[x0][y0].name != name)
1495       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1496     end
1497
1498     ## destination check
1499     if (@array[x1][y1] &&
1500         (@array[x1][y1].sente == sente)) # can't capture mine
1501       return :illegal
1502     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1503       return :illegal           # can't put on existing piece
1504     end
1505
1506     tmp_board = Marshal.load(Marshal.dump(self))
1507     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1508     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1509     tmp_board.update_sennichite(sente)
1510     os_result = tmp_board.oute_sennichite?(sente)
1511     return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
1512     return :sennichite if tmp_board.sennichite?(sente)
1513
1514     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1515       return :uchifuzume
1516     end
1517
1518     move_to(x0, y0, x1, y1, name, sente)
1519     str = to_s
1520
1521     update_sennichite(sente)
1522     return :normal
1523   end
1524
1525   def to_s
1526     a = Array::new
1527     y = 1
1528     while (y <= 9)
1529       a.push(sprintf("P%d", y))
1530       x = 9
1531       while (x >= 1)
1532         piece = @array[x][y]
1533         if (piece)
1534           s = piece.to_s
1535         else
1536           s = " * "
1537         end
1538         a.push(s)
1539         x = x - 1
1540       end
1541       a.push(sprintf("\n"))
1542       y = y + 1
1543     end
1544     if (! sente_hands.empty?)
1545       a.push("P+")
1546       sente_hands.each do |p|
1547         a.push("00" + p.name)
1548       end
1549       a.push("\n")
1550     end
1551     if (! gote_hands.empty?)
1552       a.push("P-")
1553       gote_hands.each do |p|
1554         a.push("00" + p.name)
1555       end
1556       a.push("\n")
1557     end
1558     a.push("+\n")
1559     return a.join
1560   end
1561 end
1562
1563 class GameResult
1564   attr_reader :players, :black, :white
1565
1566   def initialize(p1, p2)
1567     @players = [p1, p2]
1568     if p1.sente && !p2.sente
1569       @black, @white = p1, p2
1570     elsif !p1.sente && p2.sente
1571       @black, @white = p2, p1
1572     else
1573       raise "Never reached!"
1574     end
1575   end
1576 end
1577
1578 class GameResultWin < GameResult
1579   attr_reader :winner, :loser
1580
1581   def initialize(winner, loser)
1582     super
1583     @winner, @loser = winner, loser
1584   end
1585
1586   def to_s
1587     black_name = @black.id || @black.name
1588     white_name = @white.id || @white.name
1589     "%s:%s" % [black_name, white_name]
1590   end
1591 end
1592
1593 class GameResultDraw < GameResult
1594
1595 end
1596
1597 class Game
1598   @@mutex = Mutex.new
1599   @@time  = 0
1600
1601   def initialize(game_name, player0, player1)
1602     @monitors = Array::new
1603     @game_name = game_name
1604     if (@game_name =~ /-(\d+)-(\d+)$/)
1605       @total_time = $1.to_i
1606       @byoyomi = $2.to_i
1607     end
1608
1609     if (player0.sente)
1610       @sente = player0
1611       @gote = player1
1612     else
1613       @sente = player1
1614       @gote = player0
1615     end
1616     @current_player = @sente
1617     @next_player = @gote
1618
1619     @sente.game = self
1620     @gote.game = self
1621
1622     @last_move = ""
1623     @current_turn = 0
1624
1625     @sente.status = "agree_waiting"
1626     @gote.status = "agree_waiting"
1627     
1628     @id = sprintf("%s+%s+%s+%s+%s", 
1629                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1630     @logfile = File.join(LEAGUE.dir, @id + ".csa")
1631
1632     LEAGUE.games[@id] = self
1633
1634     log_message(sprintf("game created %s", @id))
1635
1636     @board = Board::new
1637     @board.initial
1638     @start_time = nil
1639     @fh = nil
1640     @result = nil
1641
1642     propose
1643   end
1644   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1645   attr_accessor :last_move, :current_turn
1646   attr_reader   :result
1647
1648   def rated?
1649     @sente.rated? && @gote.rated?
1650   end
1651
1652   def monitoron(monitor)
1653     @monitors.delete(monitor)
1654     @monitors.push(monitor)
1655   end
1656
1657   def monitoroff(monitor)
1658     @monitors.delete(monitor)
1659   end
1660
1661   def reject(rejector)
1662     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1663     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1664     finish
1665   end
1666
1667   def kill(killer)
1668     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1669       reject(killer.name)
1670     elsif (@current_player == killer)
1671       abnormal_lose()
1672       finish
1673     end
1674   end
1675
1676   def finish
1677     log_message(sprintf("game finished %s", @id))
1678     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1679     @fh.close
1680
1681     @sente.game = nil
1682     @gote.game = nil
1683     @sente.status = "connected"
1684     @gote.status = "connected"
1685
1686     if (@current_player.protocol == LoginCSA::PROTOCOL)
1687       @current_player.finish
1688     end
1689     if (@next_player.protocol == LoginCSA::PROTOCOL)
1690       @next_player.finish
1691     end
1692     @monitors = Array::new
1693     @sente = nil
1694     @gote = nil
1695     @current_player = nil
1696     @next_player = nil
1697     LEAGUE.games.delete(@id)
1698   end
1699
1700   def handle_one_move(str, player)
1701     finish_flag = true
1702     if (@current_player == player)
1703       @end_time = Time::new
1704       t = (@end_time - @start_time).floor
1705       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1706       
1707       move_status = nil
1708       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1709         status = :timeout
1710       elsif (str == :timeout)
1711         return false            # time isn't expired. players aren't swapped. continue game
1712       else
1713         @current_player.mytime = @current_player.mytime - t
1714         if (@current_player.mytime < 0)
1715           @current_player.mytime = 0
1716         end
1717
1718 #        begin
1719           move_status = @board.handle_one_move(str, @sente == @current_player)
1720 #        rescue
1721 #          log_error("handle_one_move raise exception for #{str}")
1722 #          move_status = :illegal
1723 #        end
1724
1725         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1726           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1727         else
1728           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite_sente_lose) || (move_status == :oute_sennichite_gote_lose))
1729             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1730             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1731             @fh.printf("%s\nT%d\n", str, t)
1732             @last_move = sprintf("%s,T%d", str, t)
1733             @current_turn = @current_turn + 1
1734           end
1735
1736           @monitors.each do |monitor|
1737             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1738             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1739           end
1740         end
1741       end
1742
1743       if (@next_player.status != "game") # rival is logout or disconnected
1744         abnormal_win()
1745       elsif (status == :timeout)
1746         timeout_lose()
1747       elsif (move_status == :illegal)
1748         illegal_lose()
1749       elsif (move_status == :kachi_win)
1750         kachi_win()
1751       elsif (move_status == :kachi_lose)
1752         kachi_lose()
1753       elsif (move_status == :toryo)
1754         toryo_lose()
1755       elsif (move_status == :outori)
1756         outori_win()
1757       elsif (move_status == :oute_sennichite_sente_lose)
1758         oute_sennichite_win_lose(@gote, @sente) # Sente is checking
1759       elsif (move_status == :oute_sennichite_gote_lose)
1760         oute_sennichite_win_lose(@sente, @gote) # Gote is checking
1761       elsif (move_status == :sennichite)
1762         sennichite_draw()
1763       elsif (move_status == :uchifuzume)
1764         uchifuzume_lose()
1765       elsif (move_status == :oute_kaihimore)
1766         oute_kaihimore_lose()
1767       else
1768         finish_flag = false
1769       end
1770       finish() if finish_flag
1771       (@current_player, @next_player) = [@next_player, @current_player]
1772       @start_time = Time::new
1773       return finish_flag
1774     end
1775   end
1776
1777   def abnormal_win
1778     @current_player.status = "connected"
1779     @next_player.status = "connected"
1780     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1781     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1782     @fh.printf("%%TORYO\n")
1783     @fh.print(@board.to_s.gsub(/^/, "\'"))
1784     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1785     @result = GameResultWin.new(@current_player, @next_player)
1786     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1787     @monitors.each do |monitor|
1788       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1789     end
1790   end
1791
1792   def abnormal_lose
1793     @current_player.status = "connected"
1794     @next_player.status = "connected"
1795     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1796     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1797     @fh.printf("%%TORYO\n")
1798     @fh.print(@board.to_s.gsub(/^/, "\'"))
1799     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1800     @result = GameResultWin.new(@next_player, @current_player)
1801     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1802     @monitors.each do |monitor|
1803       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1804     end
1805   end
1806
1807   def sennichite_draw
1808     @current_player.status = "connected"
1809     @next_player.status = "connected"
1810     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1811     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1812     @fh.print(@board.to_s.gsub(/^/, "\'"))
1813     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1814     @result = GameResultDraw.new(@current_player, @next_player)
1815     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1816     @monitors.each do |monitor|
1817       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1818     end
1819   end
1820
1821   def oute_sennichite_win_lose(winner, loser)
1822     @current_player.status = "connected"
1823     @next_player.status = "connected"
1824     loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1825     winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1826     @fh.print(@board.to_s.gsub(/^/, "\'"))
1827     if loser == @current_player
1828       @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1829     else
1830       @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name)
1831     end
1832     @result = GameResultWin.new(winner, loser)
1833     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1834     @monitors.each do |monitor|
1835       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1836     end
1837   end
1838
1839   def illegal_lose
1840     @current_player.status = "connected"
1841     @next_player.status = "connected"
1842     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1843     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1844     @fh.print(@board.to_s.gsub(/^/, "\'"))
1845     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1846     @result = GameResultWin.new(@next_player, @current_player)
1847     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1848     @monitors.each do |monitor|
1849       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1850     end
1851   end
1852
1853   def uchifuzume_lose
1854     @current_player.status = "connected"
1855     @next_player.status = "connected"
1856     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1857     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1858     @fh.print(@board.to_s.gsub(/^/, "\'"))
1859     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1860     @result = GameResultWin.new(@next_player, @current_player)
1861     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1862     @monitors.each do |monitor|
1863       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1864     end
1865   end
1866
1867   def oute_kaihimore_lose
1868     @current_player.status = "connected"
1869     @next_player.status = "connected"
1870     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1871     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1872     @fh.print(@board.to_s.gsub(/^/, "\'"))
1873     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1874     @result = GameResultWin.new(@next_player, @current_player)
1875     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1876     @monitors.each do |monitor|
1877       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1878     end
1879   end
1880
1881   def timeout_lose
1882     @current_player.status = "connected"
1883     @next_player.status = "connected"
1884     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1885     @next_player.write_safe("#TIME_UP\n#WIN\n")
1886     @fh.print(@board.to_s.gsub(/^/, "\'"))
1887     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1888     @result = GameResultWin.new(@next_player, @current_player)
1889     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1890     @monitors.each do |monitor|
1891       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1892     end
1893   end
1894
1895   def kachi_win
1896     @current_player.status = "connected"
1897     @next_player.status = "connected"
1898     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1899     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1900     @fh.printf("%%KACHI\n")
1901     @fh.print(@board.to_s.gsub(/^/, "\'"))
1902     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1903     @result = GameResultWin.new(@current_player, @next_player)
1904     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1905     @monitors.each do |monitor|
1906       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1907     end
1908   end
1909
1910   def kachi_lose
1911     @current_player.status = "connected"
1912     @next_player.status = "connected"
1913     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1914     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1915     @fh.printf("%%KACHI\n")
1916     @fh.print(@board.to_s.gsub(/^/, "\'"))
1917     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1918     @result = GameResultWin.new(@next_player, @current_player)
1919     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1920     @monitors.each do |monitor|
1921       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1922     end
1923   end
1924
1925   def toryo_lose
1926     @current_player.status = "connected"
1927     @next_player.status = "connected"
1928     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1929     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1930     @fh.printf("%%TORYO\n")
1931     @fh.print(@board.to_s.gsub(/^/, "\'"))
1932     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1933     @result = GameResultWin.new(@next_player, @current_player)
1934     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1935     @monitors.each do |monitor|
1936       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1937     end
1938   end
1939
1940   def outori_win
1941     @current_player.status = "connected"
1942     @next_player.status = "connected"
1943     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1944     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1945     @fh.print(@board.to_s.gsub(/^/, "\'"))
1946     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1947     @result = GameResultWin.new(@current_player, @next_player)
1948     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1949     @monitors.each do |monitor|
1950       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1951     end
1952   end
1953
1954   def start
1955     log_message(sprintf("game started %s", @id))
1956     @sente.write_safe(sprintf("START:%s\n", @id))
1957     @gote.write_safe(sprintf("START:%s\n", @id))
1958     @sente.mytime = @total_time
1959     @gote.mytime = @total_time
1960     @start_time = Time::new
1961   end
1962
1963   def propose
1964     @fh = open(@logfile, "w")
1965     @fh.sync = true
1966
1967     @fh.puts("V2")
1968     @fh.puts("N+#{@sente.name}")
1969     @fh.puts("N-#{@gote.name}")
1970     @fh.puts("$EVENT:#{@id}")
1971
1972     @sente.write_safe(propose_message("+"))
1973     @gote.write_safe(propose_message("-"))
1974
1975     now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
1976     @fh.puts("$START_TIME:#{now}")
1977     @fh.print <<EOM
1978 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1979 P2 * -HI *  *  *  *  * -KA * 
1980 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1981 P4 *  *  *  *  *  *  *  *  * 
1982 P5 *  *  *  *  *  *  *  *  * 
1983 P6 *  *  *  *  *  *  *  *  * 
1984 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1985 P8 * +KA *  *  *  *  * +HI * 
1986 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1987 +
1988 EOM
1989   end
1990
1991   def show()
1992     str0 = <<EOM
1993 BEGIN Game_Summary
1994 Protocol_Version:1.1
1995 Protocol_Mode:Server
1996 Format:Shogi 1.0
1997 Declaration:Jishogi 1.1
1998 Game_ID:#{@id}
1999 Name+:#{@sente.name}
2000 Name-:#{@gote.name}
2001 Rematch_On_Draw:NO
2002 To_Move:+
2003 BEGIN Time
2004 Time_Unit:1sec
2005 Total_Time:#{@total_time}
2006 Byoyomi:#{@byoyomi}
2007 Least_Time_Per_Move:#{Least_Time_Per_Move}
2008 Remaining_Time+:#{@sente.mytime}
2009 Remaining_Time-:#{@gote.mytime}
2010 Last_Move:#{@last_move}
2011 Current_Turn:#{@current_turn}
2012 END Time
2013 BEGIN Position
2014 EOM
2015
2016     str1 = <<EOM
2017 END Position
2018 END Game_Summary
2019 EOM
2020
2021     return str0 + @board.to_s + str1
2022   end
2023
2024   def propose_message(sg_flag)
2025     str = <<EOM
2026 BEGIN Game_Summary
2027 Protocol_Version:1.1
2028 Protocol_Mode:Server
2029 Format:Shogi 1.0
2030 Declaration:Jishogi 1.1
2031 Game_ID:#{@id}
2032 Name+:#{@sente.name}
2033 Name-:#{@gote.name}
2034 Your_Turn:#{sg_flag}
2035 Rematch_On_Draw:NO
2036 To_Move:+
2037 BEGIN Time
2038 Time_Unit:1sec
2039 Total_Time:#{@total_time}
2040 Byoyomi:#{@byoyomi}
2041 Least_Time_Per_Move:#{Least_Time_Per_Move}
2042 END Time
2043 BEGIN Position
2044 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
2045 P2 * -HI *  *  *  *  * -KA * 
2046 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
2047 P4 *  *  *  *  *  *  *  *  * 
2048 P5 *  *  *  *  *  *  *  *  * 
2049 P6 *  *  *  *  *  *  *  *  * 
2050 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
2051 P8 * +KA *  *  *  *  * +HI * 
2052 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
2053 P+
2054 P-
2055 +
2056 END Position
2057 END Game_Summary
2058 EOM
2059     return str
2060   end
2061   
2062   private
2063   
2064   def issue_current_time
2065     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
2066     @@mutex.synchronize do
2067       while time <= @@time do
2068         time += 1
2069       end
2070       @@time = time
2071     end
2072   end
2073 end
2074 end # module ShogiServer
2075
2076 #################################################
2077 # MAIN
2078 #
2079
2080 def usage
2081     print <<EOM
2082 NAME
2083         shogi-server - server for CSA server protocol
2084
2085 SYNOPSIS
2086         shogi-server [OPTIONS] event_name port_number
2087
2088 DESCRIPTION
2089         server for CSA server protocol
2090
2091 OPTIONS
2092         --pid-file file
2093                 specify filename for logging process ID
2094     --daemon dir
2095         run as a daemon. Log files will be put in dir.
2096
2097 LICENSE
2098         this file is distributed under GPL version2 and might be compiled by Exerb
2099
2100 SEE ALSO
2101
2102 RELEASE
2103         #{ShogiServer::Release}
2104
2105 REVISION
2106         #{ShogiServer::Revision}
2107 EOM
2108 end
2109
2110 def log_debug(str)
2111   $logger.debug(str)
2112 end
2113
2114 def log_message(str)
2115   $logger.info(str)
2116 end
2117
2118 def log_warning(str)
2119   $logger.warn(str)
2120 end
2121
2122 def log_error(str)
2123   $logger.error(str)
2124 end
2125
2126
2127 def parse_command_line
2128   options = Hash::new
2129   parser = GetoptLong.new( ["--daemon",         GetoptLong::REQUIRED_ARGUMENT],
2130                            ["--pid-file",       GetoptLong::REQUIRED_ARGUMENT]
2131                          )
2132   parser.quiet = true
2133   begin
2134     parser.each_option do |name, arg|
2135       name.sub!(/^--/, '')
2136       options[name] = arg.dup
2137     end
2138   rescue
2139     usage
2140     raise parser.error_message
2141   end
2142   return options
2143 end
2144
2145 def write_pid_file(file)
2146   open(file, "w") do |fh|
2147     fh.print Process::pid, "\n"
2148   end
2149 end
2150
2151 def mutex_watchdog(mutex, sec)
2152   while true
2153     begin
2154       timeout(sec) do
2155         begin
2156           mutex.lock
2157         ensure
2158           mutex.unlock
2159         end
2160       end
2161       sleep(sec)
2162     rescue TimeoutError
2163       log_error("mutex watchdog timeout")
2164       exit(1)
2165     end
2166   end
2167 end
2168
2169 def main
2170
2171   $mutex = Mutex::new
2172   Thread::start do
2173     Thread.pass
2174     mutex_watchdog($mutex, 10)
2175   end
2176
2177   $options = parse_command_line
2178   if (ARGV.length != 2)
2179     usage
2180     exit 2
2181   end
2182
2183   LEAGUE.event = ARGV.shift
2184   port = ARGV.shift
2185
2186   write_pid_file($options["pid-file"]) if ($options["pid-file"])
2187
2188   dir = $options["daemon"] || nil
2189   if dir && ! File.exist?(dir)
2190     FileUtils.mkdir(dir)
2191   end
2192   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2193   $logger = WEBrick::Log.new(log_file)
2194
2195   LEAGUE.dir = dir || File.dirname(__FILE__)
2196   LEAGUE.setup_players_database
2197
2198   config = {}
2199   config[:Port]       = port
2200   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2201   config[:Logger]     = $logger
2202
2203   server = WEBrick::GenericServer.new(config)
2204   ["INT", "TERM"].each do |signal| 
2205     trap(signal) do
2206       LEAGUE.shutdown
2207       server.shutdown
2208     end
2209   end
2210   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
2211   log_message("server started [Revision: #{ShogiServer::Revision}]")
2212
2213   server.start do |client|
2214       # client.sync = true # this is already set in WEBrick 
2215       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2216         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2217       player = nil
2218       login  = nil
2219       while (str = client.gets_timeout(ShogiServer::Login_Time))
2220         begin
2221           $mutex.lock
2222           str =~ /([\r\n]*)$/
2223           eol = $1
2224           if (ShogiServer::Login::good_login?(str))
2225             player = ShogiServer::Player::new(str, client)
2226             player.eol = eol
2227             login  = ShogiServer::Login::factory(str, player)
2228             if (LEAGUE.players[player.name])
2229               if ((LEAGUE.players[player.name].password == player.password) &&
2230                   (LEAGUE.players[player.name].status != "game"))
2231                 log_message(sprintf("user %s login forcely", player.name))
2232                 LEAGUE.players[player.name].kill
2233               else
2234                 login.incorrect_duplicated_player(str)
2235                 #Thread::exit
2236                 #return
2237                 # TODO
2238                 player = nil
2239                 break
2240               end
2241             end
2242             LEAGUE.add(player)
2243             break
2244           else
2245             client.write_safe("LOGIN:incorrect" + eol)
2246             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2247           end
2248         ensure
2249           $mutex.unlock
2250         end
2251       end                       # login loop
2252       if (! player)
2253         #client.close
2254         #Thread::exit
2255         #return
2256         next
2257       end
2258       log_message(sprintf("user %s login", player.name))
2259       login.process
2260       player.run(login.csa_1st_str)
2261       begin
2262         $mutex.lock
2263         if (player.game)
2264           player.game.kill(player)
2265         end
2266         player.finish # socket has been closed
2267         LEAGUE.delete(player)
2268         log_message(sprintf("user %s logout", player.name))
2269       ensure
2270         $mutex.unlock
2271       end
2272   end
2273 end
2274
2275
2276 if ($0 == __FILE__)
2277   STDOUT.sync = true
2278   STDERR.sync = true
2279   TCPSocket.do_not_reverse_lookup = true
2280   Thread.abort_on_exception = true
2281
2282   LEAGUE = ShogiServer::League::new
2283   main
2284 end