4 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
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.
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.
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
20 Max_Write_Queue_Size = 1000
21 Max_Identifier_Length = 32
22 Default_Timeout = 60 # for single socket operation
24 Default_Game_Name = "default-1500-0"
27 Least_Time_Per_Move = 1
28 Login_Time = 300 # time for LOGIN
30 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
31 Release.concat("-") if (Release == "")
32 Revision = "$Revision$".gsub(/[^\.\d]/, '')
45 TCPSocket.do_not_reverse_lookup = true
46 Thread.abort_on_exception = true
50 def gets_timeout(t = Default_Timeout)
61 def gets_safe(t = nil)
82 return self.write(str)
95 @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") )
97 attr_accessor :players, :games, :event
100 self.load(player) if player.id
101 @players[player.name] = player
105 @players.delete(player.name)
108 def get_player(status, game_name, sente, searcher=nil)
109 @players.each do |name, player|
110 if ((player.status == status) &&
111 (player.game_name == game_name) &&
112 ((sente == nil) || (player.sente == nil) || (player.sente == sente)) &&
113 ((searcher == nil) || (player != searcher)))
121 hash = search(player.id)
124 player.name = hash['name']
125 player.rate = hash['rate']
126 player.modified_at = hash['modified_at']
140 @db.transaction(true) do
141 @db.roots.each do |id|
145 return players.collect do |id|
156 # Idetifier of the player in the rating system
160 # Score in the rating sysem
162 # Last timestamp when the rate was modified
163 attr_accessor :modified_at
166 @modified_at || Time.now
172 @modified_at = Time.now
181 class Player < RatedPlayer
182 def initialize(str, socket)
186 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
188 @protocol = nil # CSA or x1
189 @eol = "\m" # favorite eol code
192 @mytime = 0 # set in start method also
196 @write_queue = Queue::new
200 attr_accessor :password, :socket, :status
201 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
202 attr_accessor :main_thread, :writer_thread, :write_queue
205 log_message(sprintf("user %s killed", @name))
210 Thread::kill(@main_thread) if @main_thread
214 if (@status != "finished")
216 log_message(sprintf("user %s finish", @name))
217 Thread::kill(@writer_thread) if @writer_thread
219 @socket.close if (! @socket.closed?)
221 log_message(sprintf("user %s finish failed", @name))
227 @write_queue.push(str.gsub(/[\r\n]+/, @eol))
231 while (str = @write_queue.pop)
232 @socket.write_safe(str)
237 if ((status == "game_waiting") ||
238 (status == "start_waiting") ||
239 (status == "agree_waiting") ||
242 return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
243 elsif (@sente == false)
244 return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
245 elsif (@sente == nil)
246 return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
249 return sprintf("%s %s %s", @name, @protocol, @status)
254 @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
261 (login, @name, @password, ext) = str.split
262 @name, trip = @name.split(",") # used by rating
263 @id = trip ? "%s+%s" % [@name, Digest::MD5.hexdigest(trip)] : nil
269 @main_thread = Thread::current
270 @writer_thread = Thread::start do
276 write_safe(sprintf("LOGIN:%s OK\n", @name))
277 if (@protocol != "CSA")
278 log_message(sprintf("user %s run in %s mode", @name, @protocol))
279 write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
281 log_message(sprintf("user %s run in CSA mode", @name))
282 if (good_game_name?(@password))
283 csa_1st_str = "%%GAME #{@password} *"
285 csa_1st_str = "%%GAME #{Default_Game_Name} *"
289 while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
296 if (@write_queue.size > Max_Write_Queue_Size)
297 log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
301 if (@status == "finished")
304 str.chomp! if (str.class == String)
307 if (@status == "game")
308 array_str = str.split(",")
309 move = array_str.shift
310 additional = array_str.shift
311 if /^'(.*)/ =~ additional
312 comment = array_str.unshift("'*#{$1}")
314 s = @game.handle_one_move(move, self)
315 @game.fh.print("#{comment}\n") if (comment && !s)
316 return if (s && @protocol == "CSA")
318 when /^%[^%]/, :timeout
319 if (@status == "game")
320 s = @game.handle_one_move(str, self)
321 return if (s && @protocol == "CSA")
324 if (@status == "agree_waiting")
326 return if (@protocol == "CSA")
328 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
331 if (@status == "agree_waiting")
332 @status = "start_waiting"
333 if ((@game.sente.status == "start_waiting") &&
334 (@game.gote.status == "start_waiting"))
336 @game.sente.status = "game"
337 @game.gote.status = "game"
340 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
342 when /^%%SHOW\s+(\S+)/
344 if (LEAGUE.games[game_id])
345 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
347 write_safe("##[SHOW] +OK\n")
348 when /^%%MONITORON\s+(\S+)/
350 if (LEAGUE.games[game_id])
351 LEAGUE.games[game_id].monitoron(self)
352 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
353 write_safe("##[MONITOR][#{game_id}] +OK\n")
355 when /^%%MONITOROFF\s+(\S+)/
357 if (LEAGUE.games[game_id])
358 LEAGUE.games[game_id].monitoroff(self)
363 players = LEAGUE.rated_players
364 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
365 write_safe("##[RATING] %s (%s) \t %4d @%s\n" %
366 [p.name, p.id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
368 write_safe("##[RATING] +OK\n")
370 if ((@status == "connected") || (@status == "game_waiting"))
371 @status = "connected"
374 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
376 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
380 if (! good_game_name?(game_name))
381 write_safe(sprintf("##[ERROR] bad game name\n"))
383 elsif ((@status == "connected") || (@status == "game_waiting"))
386 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
389 if ((my_sente_str == "*") ||
390 (my_sente_str == "+") ||
391 (my_sente_str == "-"))
394 write_safe(sprintf("##[ERROR] bad game option\n"))
398 if (my_sente_str == "*")
399 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
400 elsif (my_sente_str == "+")
401 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
402 elsif (my_sente_str == "-")
403 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
408 @game_name = game_name
409 if ((my_sente_str == "*") && (rival.sente == nil))
417 elsif (rival.sente == true) # rival has higher priority
419 elsif (rival.sente == false)
421 elsif (my_sente_str == "+")
424 elsif (my_sente_str == "-")
430 Game::new(@game_name, self, rival)
431 self.status = "agree_waiting"
432 rival.status = "agree_waiting"
433 else # rival not found
434 if (command_name == "GAME")
435 @status = "game_waiting"
436 @game_name = game_name
437 if (my_sente_str == "+")
439 elsif (my_sente_str == "-")
445 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
446 @status = "connected"
451 when /^%%CHAT\s+(.+)/
453 LEAGUE.players.each do |name, player|
454 if (player.protocol != "CSA")
455 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
460 LEAGUE.games.each do |id, game|
461 buf.push(sprintf("##[LIST] %s\n", id))
463 buf.push("##[LIST] +OK\n")
467 LEAGUE.players.each do |name, player|
468 buf.push(sprintf("##[WHO] %s\n", player.to_s))
470 buf.push("##[WHO] +OK\n")
473 @status = "connected"
474 write_safe("LOGOUT:completed\n")
477 ## ignore null string
479 write_safe(sprintf("##[ERROR] unknown command %s\n", str))
489 PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
490 def initialize(board, x, y, sente, promoted=false)
497 if ((x == 0) || (y == 0))
499 hands = board.sente_hands
501 hands = board.gote_hands
508 @board.array[x][y] = self
511 attr_accessor :promoted, :sente, :x, :y, :board
513 def room_of_head?(x, y, name)
518 return adjacent_movable_grids + far_movable_grids
521 def far_movable_grids
526 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
527 if ((@board.array[x][y] == nil) || # dst is empty
528 (@board.array[x][y].sente != @sente)) # dst is enemy
536 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
537 if (@board.array[x][y] == nil) # dst is empty?
544 def adjacent_movable_grids
547 moves = @promoted_moves
549 moves = @normal_moves
551 moves.each do |(dx, dy)|
558 if (jump_to?(cand_x, cand_y))
559 grids.push([cand_x, cand_y])
565 def move_to?(x, y, name)
566 return false if (! room_of_head?(x, y, name))
567 return false if ((name != @name) && (name != @promoted_name))
568 return false if (@promoted && (name != @promoted_name)) # can't un-promote
571 return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
573 return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
575 return false if ((6 >= @y) && (6 >= y) && (name != @name))
579 if ((@x == 0) || (@y == 0))
580 return jump_to?(x, y)
582 return movable_grids.include?([x, y])
587 if ((@x == 0) || (@y == 0))
589 @board.sente_hands.delete(self)
591 @board.gote_hands.delete(self)
593 @board.array[x][y] = self
594 elsif ((x == 0) || (y == 0))
595 @promoted = false # clear promoted flag before moving to hands
597 @board.sente_hands.push(self)
599 @board.gote_hands.push(self)
601 @board.array[@x][@y] = nil
603 @board.array[@x][@y] = nil
604 @board.array[x][y] = self
637 class PieceFU < Piece
640 @normal_moves = [[0, +1]]
641 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
643 @promoted_name = "TO"
646 def room_of_head?(x, y, name)
649 return false if (y == 1)
651 return false if (y == 9)
657 if ((iy != @y) && # not source position
658 @board.array[x][iy] &&
659 (@board.array[x][iy].sente == @sente) && # mine
660 (@board.array[x][iy].name == "FU") &&
661 (@board.array[x][iy].promoted == false))
671 class PieceKY < Piece
675 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
677 @promoted_name = "NY"
680 def room_of_head?(x, y, name)
683 return false if (y == 1)
685 return false if (y == 9)
690 def far_movable_grids
698 while (jump_to?(cand_x, cand_y))
699 grids.push([cand_x, cand_y])
700 break if (! put_to?(cand_x, cand_y))
706 while (jump_to?(cand_x, cand_y))
707 grids.push([cand_x, cand_y])
708 break if (! put_to?(cand_x, cand_y))
716 class PieceKE < Piece
719 @normal_moves = [[+1, +2], [-1, +2]]
720 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
722 @promoted_name = "NK"
725 def room_of_head?(x, y, name)
728 return false if ((y == 1) || (y == 2))
730 return false if ((y == 9) || (y == 8))
736 class PieceGI < Piece
739 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
740 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
742 @promoted_name = "NG"
746 class PieceKI < Piece
749 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
756 class PieceKA < Piece
760 @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
762 @promoted_name = "UM"
765 def far_movable_grids
770 while (jump_to?(cand_x, cand_y))
771 grids.push([cand_x, cand_y])
772 break if (! put_to?(cand_x, cand_y))
779 while (jump_to?(cand_x, cand_y))
780 grids.push([cand_x, cand_y])
781 break if (! put_to?(cand_x, cand_y))
788 while (jump_to?(cand_x, cand_y))
789 grids.push([cand_x, cand_y])
790 break if (! put_to?(cand_x, cand_y))
797 while (jump_to?(cand_x, cand_y))
798 grids.push([cand_x, cand_y])
799 break if (! put_to?(cand_x, cand_y))
806 class PieceHI < Piece
810 @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
812 @promoted_name = "RY"
815 def far_movable_grids
820 while (jump_to?(cand_x, cand_y))
821 grids.push([cand_x, cand_y])
822 break if (! put_to?(cand_x, cand_y))
828 while (jump_to?(cand_x, cand_y))
829 grids.push([cand_x, cand_y])
830 break if (! put_to?(cand_x, cand_y))
836 while (jump_to?(cand_x, cand_y))
837 grids.push([cand_x, cand_y])
838 break if (! put_to?(cand_x, cand_y))
844 while (jump_to?(cand_x, cand_y))
845 grids.push([cand_x, cand_y])
846 break if (! put_to?(cand_x, cand_y))
852 class PieceOU < Piece
855 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
865 @sente_hands = Array::new
866 @gote_hands = Array::new
868 @sente_history = Hash::new
869 @gote_history = Hash::new
870 @array = [[], [], [], [], [], [], [], [], [], []]
873 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
874 attr_reader :move_count
877 PieceKY::new(self, 1, 1, false)
878 PieceKE::new(self, 2, 1, false)
879 PieceGI::new(self, 3, 1, false)
880 PieceKI::new(self, 4, 1, false)
881 PieceOU::new(self, 5, 1, false)
882 PieceKI::new(self, 6, 1, false)
883 PieceGI::new(self, 7, 1, false)
884 PieceKE::new(self, 8, 1, false)
885 PieceKY::new(self, 9, 1, false)
886 PieceKA::new(self, 2, 2, false)
887 PieceHI::new(self, 8, 2, false)
888 PieceFU::new(self, 1, 3, false)
889 PieceFU::new(self, 2, 3, false)
890 PieceFU::new(self, 3, 3, false)
891 PieceFU::new(self, 4, 3, false)
892 PieceFU::new(self, 5, 3, false)
893 PieceFU::new(self, 6, 3, false)
894 PieceFU::new(self, 7, 3, false)
895 PieceFU::new(self, 8, 3, false)
896 PieceFU::new(self, 9, 3, false)
898 PieceKY::new(self, 1, 9, true)
899 PieceKE::new(self, 2, 9, true)
900 PieceGI::new(self, 3, 9, true)
901 PieceKI::new(self, 4, 9, true)
902 PieceOU::new(self, 5, 9, true)
903 PieceKI::new(self, 6, 9, true)
904 PieceGI::new(self, 7, 9, true)
905 PieceKE::new(self, 8, 9, true)
906 PieceKY::new(self, 9, 9, true)
907 PieceKA::new(self, 8, 8, true)
908 PieceHI::new(self, 2, 8, true)
909 PieceFU::new(self, 1, 7, true)
910 PieceFU::new(self, 2, 7, true)
911 PieceFU::new(self, 3, 7, true)
912 PieceFU::new(self, 4, 7, true)
913 PieceFU::new(self, 5, 7, true)
914 PieceFU::new(self, 6, 7, true)
915 PieceFU::new(self, 7, 7, true)
916 PieceFU::new(self, 8, 7, true)
917 PieceFU::new(self, 9, 7, true)
920 def have_piece?(hands, name)
921 piece = hands.find { |i|
927 def move_to(x0, y0, x1, y1, name, sente)
934 if ((x0 == 0) || (y0 == 0))
935 piece = have_piece?(hands, name)
936 return :illegal if (! piece.move_to?(x1, y1, name))
937 piece.move_to(x1, y1)
939 return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
940 if (@array[x0][y0].name != name) # promoted ?
941 @array[x0][y0].promoted = true
944 if (@array[x1][y1].name == "OU")
945 return :outori # return board update
947 @array[x1][y1].sente = @array[x0][y0].sente
948 @array[x1][y1].move_to(0, 0)
953 @array[x0][y0].move_to(x1, y1)
959 def look_for_ou(sente)
965 (@array[x][y].name == "OU") &&
966 (@array[x][y].sente == sente))
973 raise "can't find ou"
976 def checkmated?(sente) # sente is loosing
977 ou = look_for_ou(sente)
983 (@array[x][y].sente != sente))
984 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
995 def uchifuzume?(sente)
996 rival_ou = look_for_ou(! sente) # rival's ou
997 if (sente) # rival is gote
998 if ((rival_ou.y != 9) &&
999 (@array[rival_ou.x][rival_ou.y + 1]) &&
1000 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1001 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1003 fu_y = rival_ou.y + 1
1008 if ((rival_ou.y != 0) &&
1009 (@array[rival_ou.x][rival_ou.y - 1]) &&
1010 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1011 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1013 fu_y = rival_ou.y - 1
1019 ## case: rival_ou is moving
1021 rival_ou.movable_grids.each do |(cand_x, cand_y)|
1022 tmp_board = Marshal.load(Marshal.dump(self))
1023 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1024 raise "internal error" if (s != true)
1025 if (! tmp_board.checkmated?(! sente)) # good move
1030 ## case: rival is capturing fu
1036 (@array[x][y].sente != sente) &&
1037 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1038 if (@array[x][y].promoted)
1039 name = @array[x][y].promoted_name
1041 name = @array[x][y].name
1043 tmp_board = Marshal.load(Marshal.dump(self))
1044 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1045 raise "internal error" if (s != true)
1046 if (! tmp_board.checkmated?(! sente)) # good move
1057 def oute_sennichite?(sente)
1058 if (checkmated?(! sente))
1061 if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1065 if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1073 def sennichite?(sente)
1075 if (@history[str] && (@history[str] >= 3)) # already 3 times
1081 def good_kachi?(sente)
1082 if (checkmated?(sente))
1083 puts "'NG: Checkmating." if $DEBUG
1087 ou = look_for_ou(sente)
1088 if (sente && (ou.y >= 4))
1089 puts "'NG: Black's OU does not enter yet." if $DEBUG
1092 if (! sente && (ou.y <= 6))
1093 puts "'NG: White's OU does not enter yet." if $DEBUG
1101 hands = @sente_hands
1111 (@array[x][y].sente == sente) &&
1112 (@array[x][y].point > 0))
1113 point = point + @array[x][y].point
1119 hands.each do |piece|
1120 point = point + piece.point
1124 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1129 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1134 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1139 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1143 def handle_one_move(str, sente=nil)
1144 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1151 elsif (str =~ /^%KACHI/)
1152 raise ArgumentError, "sente is null", caller unless sente
1153 if (good_kachi?(sente))
1158 elsif (str =~ /^%TORYO/)
1164 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1165 ((x0 != 0) || (y0 != 0)))
1167 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1173 hands = @sente_hands
1180 if ((x0 == 0) && (y0 == 0))
1181 return :illegal if (! have_piece?(hands, name))
1182 elsif (! @array[x0][y0])
1183 return :illegal # no piece
1184 elsif (@array[x0][y0].sente != sente)
1185 return :illegal # this is not mine
1186 elsif (@array[x0][y0].name != name)
1187 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1190 ## destination check
1191 if (@array[x1][y1] &&
1192 (@array[x1][y1].sente == sente)) # can't capture mine
1194 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1195 return :illegal # can't put on existing piece
1198 tmp_board = Marshal.load(Marshal.dump(self))
1199 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1200 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1201 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1202 return :sennichite if tmp_board.sennichite?(sente)
1204 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1208 move_to(x0, y0, x1, y1, name, sente)
1211 if (checkmated?(! sente))
1213 @sente_history[str] = (@sente_history[str] || 0) + 1
1215 @gote_history[str] = (@gote_history[str] || 0) + 1
1219 @sente_history.clear
1224 @history[str] = (@history[str] || 0) + 1
1232 a.push(sprintf("P%d", y))
1235 piece = @array[x][y]
1244 a.push(sprintf("\n"))
1247 if (! sente_hands.empty?)
1249 sente_hands.each do |p|
1250 a.push("00" + p.name)
1254 if (! gote_hands.empty?)
1256 gote_hands.each do |p|
1257 a.push("00" + p.name)
1267 attr_reader :players, :black, :white
1269 def initialize(p1, p2)
1273 if p1.sente && !p2.sente
1274 @black, @white = p1, p2
1275 elsif !p1.sente && p2.sente
1276 @black, @white = p2, p1
1278 raise "Never reached!"
1283 class GameResultWin < GameResult
1284 attr_reader :winner, :loser
1286 def initialize(winner, loser)
1288 @winner, @loser = winner, loser
1292 black_name = @black.id || @black.name
1293 white_name = @white.id || @white.name
1294 "%s:%s" % [black_name, white_name]
1298 class GameResultDraw < GameResult
1306 def initialize(game_name, player0, player1)
1307 @monitors = Array::new
1308 @game_name = game_name
1309 if (@game_name =~ /-(\d+)-(\d+)$/)
1310 @total_time = $1.to_i
1321 @current_player = @sente
1322 @next_player = @gote
1330 @sente.status = "agree_waiting"
1331 @gote.status = "agree_waiting"
1333 @id = sprintf("%s+%s+%s+%s+%s",
1334 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1335 @logfile = @id + ".csa"
1337 LEAGUE.games[@id] = self
1339 log_message(sprintf("game created %s", @id))
1349 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1350 attr_accessor :last_move, :current_turn
1354 @sente.rated? && @gote.rated?
1357 def monitoron(monitor)
1358 @monitors.delete(monitor)
1359 @monitors.push(monitor)
1362 def monitoroff(monitor)
1363 @monitors.delete(monitor)
1366 def reject(rejector)
1367 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1368 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1373 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1375 elsif (@current_player == killer)
1382 log_message(sprintf("game finished %s", @id))
1383 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1388 @sente.status = "connected"
1389 @gote.status = "connected"
1391 if (@current_player.protocol == "CSA")
1392 @current_player.finish
1394 if (@next_player.protocol == "CSA")
1397 @monitors = Array::new
1400 @current_player = nil
1402 LEAGUE.games.delete(@id)
1405 def handle_one_move(str, player)
1407 if (@current_player == player)
1408 @end_time = Time::new
1409 t = (@end_time - @start_time).floor
1410 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1413 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1415 elsif (str == :timeout)
1416 return false # time isn't expired. players aren't swapped. continue game
1418 @current_player.mytime = @current_player.mytime - t
1419 if (@current_player.mytime < 0)
1420 @current_player.mytime = 0
1424 move_status = @board.handle_one_move(str, @sente == @current_player)
1426 # log_error("handle_one_move raise exception for #{str}")
1427 # move_status = :illegal
1430 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1431 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1433 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1434 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1435 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1436 @fh.printf("%s\nT%d\n", str, t)
1437 @last_move = sprintf("%s,T%d", str, t)
1438 @current_turn = @current_turn + 1
1441 @monitors.each do |monitor|
1442 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1443 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1448 if (@next_player.status != "game") # rival is logout or disconnected
1450 elsif (status == :timeout)
1452 elsif (move_status == :illegal)
1454 elsif (move_status == :kachi_win)
1456 elsif (move_status == :kachi_lose)
1458 elsif (move_status == :toryo)
1460 elsif (move_status == :outori)
1462 elsif (move_status == :sennichite)
1464 elsif (move_status == :oute_sennichite)
1465 oute_sennichite_lose()
1466 elsif (move_status == :uchifuzume)
1468 elsif (move_status == :oute_kaihimore)
1469 oute_kaihimore_lose()
1473 finish() if finish_flag
1474 (@current_player, @next_player) = [@next_player, @current_player]
1475 @start_time = Time::new
1481 @current_player.status = "connected"
1482 @next_player.status = "connected"
1483 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1484 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1485 @fh.printf("%%TORYO\n")
1486 @fh.print(@board.to_s.gsub(/^/, "\'"))
1487 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1488 @result = GameResultWin.new(@current_player, @next_player)
1489 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1490 @monitors.each do |monitor|
1491 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1496 @current_player.status = "connected"
1497 @next_player.status = "connected"
1498 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1499 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1500 @fh.printf("%%TORYO\n")
1501 @fh.print(@board.to_s.gsub(/^/, "\'"))
1502 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1503 @result = GameResultWin.new(@next_player, @current_player)
1504 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1505 @monitors.each do |monitor|
1506 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1511 @current_player.status = "connected"
1512 @next_player.status = "connected"
1513 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1514 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1515 @fh.print(@board.to_s.gsub(/^/, "\'"))
1516 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1517 @result = GameResultDraw.new(@current_player, @next_player)
1518 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1519 @monitors.each do |monitor|
1520 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1524 def oute_sennichite_lose
1525 @current_player.status = "connected"
1526 @next_player.status = "connected"
1527 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1528 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1529 @fh.print(@board.to_s.gsub(/^/, "\'"))
1530 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1531 @result = GameResultWin.new(@next_player, @current_player)
1532 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1533 @monitors.each do |monitor|
1534 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1539 @current_player.status = "connected"
1540 @next_player.status = "connected"
1541 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1542 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1543 @fh.print(@board.to_s.gsub(/^/, "\'"))
1544 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1545 @result = GameResultWin.new(@next_player, @current_player)
1546 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1547 @monitors.each do |monitor|
1548 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1553 @current_player.status = "connected"
1554 @next_player.status = "connected"
1555 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1556 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1557 @fh.print(@board.to_s.gsub(/^/, "\'"))
1558 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1559 @result = GameResultWin.new(@next_player, @current_player)
1560 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1561 @monitors.each do |monitor|
1562 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1566 def oute_kaihimore_lose
1567 @current_player.status = "connected"
1568 @next_player.status = "connected"
1569 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1570 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1571 @fh.print(@board.to_s.gsub(/^/, "\'"))
1572 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1573 @result = GameResultWin.new(@next_player, @current_player)
1574 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1575 @monitors.each do |monitor|
1576 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1581 @current_player.status = "connected"
1582 @next_player.status = "connected"
1583 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1584 @next_player.write_safe("#TIME_UP\n#WIN\n")
1585 @fh.print(@board.to_s.gsub(/^/, "\'"))
1586 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1587 @result = GameResultWin.new(@next_player, @current_player)
1588 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1589 @monitors.each do |monitor|
1590 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1595 @current_player.status = "connected"
1596 @next_player.status = "connected"
1597 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1598 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1599 @fh.printf("%%KACHI\n")
1600 @fh.print(@board.to_s.gsub(/^/, "\'"))
1601 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1602 @result = GameResultWin.new(@current_player, @next_player)
1603 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1604 @monitors.each do |monitor|
1605 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1610 @current_player.status = "connected"
1611 @next_player.status = "connected"
1612 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1613 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1614 @fh.printf("%%KACHI\n")
1615 @fh.print(@board.to_s.gsub(/^/, "\'"))
1616 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1617 @result = GameResultWin.new(@next_player, @current_player)
1618 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1619 @monitors.each do |monitor|
1620 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1625 @current_player.status = "connected"
1626 @next_player.status = "connected"
1627 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1628 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1629 @fh.printf("%%TORYO\n")
1630 @fh.print(@board.to_s.gsub(/^/, "\'"))
1631 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1632 @result = GameResultWin.new(@next_player, @current_player)
1633 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1634 @monitors.each do |monitor|
1635 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1640 @current_player.status = "connected"
1641 @next_player.status = "connected"
1642 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1643 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1644 @fh.print(@board.to_s.gsub(/^/, "\'"))
1645 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1646 @result = GameResultWin.new(@current_player, @next_player)
1647 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1648 @monitors.each do |monitor|
1649 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1654 log_message(sprintf("game started %s", @id))
1655 @sente.write_safe(sprintf("START:%s\n", @id))
1656 @gote.write_safe(sprintf("START:%s\n", @id))
1657 @sente.mytime = @total_time
1658 @gote.mytime = @total_time
1659 @start_time = Time::new
1664 @fh = open(@logfile, "w")
1668 @fh.printf("N+%s\n", @sente.name)
1669 @fh.printf("N-%s\n", @gote.name)
1670 @fh.printf("$EVENT:%s\n", @id)
1672 @sente.write_safe(propose_message("+"))
1673 @gote.write_safe(propose_message("-"))
1675 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1677 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1678 P2 * -HI * * * * * -KA *
1679 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1680 P4 * * * * * * * * *
1681 P5 * * * * * * * * *
1682 P6 * * * * * * * * *
1683 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1684 P8 * +KA * * * * * +HI *
1685 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1694 Protocol_Version:1.1
1695 Protocol_Mode:Server
1697 Declaration:Jishogi 1.1
1699 Name+:#{@sente.name}
1705 Total_Time:#{@total_time}
1707 Least_Time_Per_Move:#{Least_Time_Per_Move}
1708 Remaining_Time+:#{@sente.mytime}
1709 Remaining_Time-:#{@gote.mytime}
1710 Last_Move:#{@last_move}
1711 Current_Turn:#{@current_turn}
1721 return str0 + @board.to_s + str1
1724 def propose_message(sg_flag)
1727 Protocol_Version:1.1
1728 Protocol_Mode:Server
1730 Declaration:Jishogi 1.1
1732 Name+:#{@sente.name}
1734 Your_Turn:#{sg_flag}
1739 Total_Time:#{@total_time}
1741 Least_Time_Per_Move:#{Least_Time_Per_Move}
1744 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1745 P2 * -HI * * * * * -KA *
1746 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1747 P4 * * * * * * * * *
1748 P5 * * * * * * * * *
1749 P6 * * * * * * * * *
1750 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1751 P8 * +KA * * * * * +HI *
1752 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1764 def issue_current_time
1765 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1766 @@mutex.synchronize do
1767 while time <= @@time do
1775 #################################################
1782 shogi-server - server for CSA server protocol
1785 shogi-server event_name port_number
1788 server for CSA server protocol
1792 specify filename for logging process ID
1795 this file is distributed under GPL version2 and might be compiled by Exerb
1807 def log_message(str)
1808 printf("%s message: %s\n", Time::new.to_s, str)
1811 def log_warning(str)
1812 printf("%s warning: %s\n", Time::new.to_s, str)
1816 printf("%s error: %s\n", Time::new.to_s, str)
1820 def parse_command_line
1822 parser = GetoptLong.new
1823 parser.ordering = GetoptLong::REQUIRE_ORDER
1825 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1829 parser.each_option do |name, arg|
1830 name.sub!(/^--/, '')
1831 options[name] = arg.dup
1835 raise parser.error_message
1840 def good_game_name?(str)
1841 if ((str =~ /^(.+)-\d+-\d+$/) &&
1842 (good_identifier?($1)))
1849 # TODO This is also checked by good_game_name?().
1850 def good_identifier?(str)
1851 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1853 elsif str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}},[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1860 def good_login?(str)
1862 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1863 (tokens[0] == "LOGIN") &&
1864 (good_identifier?(tokens[1])))
1871 def write_pid_file(file)
1872 open(file, "w") do |fh|
1873 fh.print Process::pid, "\n"
1877 def mutex_watchdog(mutex, sec)
1889 log_error("mutex watchdog timeout")
1899 mutex_watchdog($mutex, 10)
1902 $options = parse_command_line
1903 if (ARGV.length != 2)
1908 LEAGUE.event = ARGV.shift
1911 write_pid_file($options["pid-file"]) if ($options["pid-file"])
1913 server = TCPserver.open(port)
1914 log_message("server started")
1917 Thread::start(server.accept) do |client|
1921 while (str = client.gets_timeout(Login_Time))
1926 if (good_login?(str))
1927 player = Player::new(str, client)
1928 if (LEAGUE.players[player.name])
1929 if ((LEAGUE.players[player.name].password == player.password) &&
1930 (LEAGUE.players[player.name].status != "game"))
1931 log_message(sprintf("user %s login forcely", player.name))
1932 LEAGUE.players[player.name].kill
1934 client.write_safe("LOGIN:incorrect" + eol)
1935 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1944 client.write_safe("LOGIN:incorrect" + eol)
1945 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1956 log_message(sprintf("user %s login", player.name))
1961 player.game.kill(player)
1963 player.finish # socket has been closed
1964 LEAGUE.delete(player)
1965 log_message(sprintf("user %s logout", player.name))
1974 LEAGUE = League::new