shogi-server source
Révision | 09bf2bfc8b6a996083cb3283259caf364a71a356 (tree) |
---|---|
l'heure | 2013-11-22 21:47:59 |
Auteur | Daigo Moriwaki <daigo@debi...> |
Commiter | Daigo Moriwaki |
Merge remote-tracking branch 'origin/wdoor-stable'
Conflicts:
changelog
@@ -1,3 +1,78 @@ | ||
1 | +2013-11-04 Daigo Moriwaki <daigo at debian dot org> | |
2 | + | |
3 | + * [mk_rate] | |
4 | + - Added a new option, --ignore, which is imported from | |
5 | + mk_rate-from-grep. | |
6 | + * [mk_game_results] | |
7 | + - Flush after each output line. | |
8 | + * Rleased: Revision "20131104" | |
9 | + | |
10 | +2013-09-08 Daigo Moriwaki <daigo at debian dot org> | |
11 | + | |
12 | + * [shogi-server] | |
13 | + - shogi_server/{game,time_clock}.rb: | |
14 | + When StopWatchClock is used, "Time_Unit:" of starting messages | |
15 | + in CSA protocol supplies "1min". | |
16 | + | |
17 | +2013-04-07 Daigo Moriwaki <daigo at debian dot org> | |
18 | + | |
19 | + * [shogi-server] | |
20 | + - shogi_server/{game,time_clock}.rb: | |
21 | + Adds variations of thinking time calculation: ChessClock | |
22 | + (current) and StopWatchClock (new). | |
23 | + StopWatchClock, which is usually used at official games of human | |
24 | + professional players, is a clock where thiking time less than a | |
25 | + miniute is regarded as zero. | |
26 | + To select StopWatchClock, use a special game name with "060" | |
27 | + byoyomi time. ex. "gamename_1500_060". | |
28 | + | |
29 | +2013-03-31 Daigo Moriwaki <daigo at debian dot org> | |
30 | + | |
31 | + * [shogi-server] | |
32 | + - %%FORK command: %%FORK <source_game> [<new_buoy_game>] [<nth-move>] | |
33 | + The new_buoy_game parameter is now optional. If it is not | |
34 | + supplied, Shogi-server generates a new buoy game name from | |
35 | + source_game. | |
36 | + - command.rb: More elaborate error messages for the %%GAME command. | |
37 | + | |
38 | +2013-03-20 Daigo Moriwaki <daigo at debian dot org> | |
39 | + | |
40 | + * [shogi-server] | |
41 | + - New pairing algorithm: ShogiServer::Pairing::LeastDiff | |
42 | + This pairing algorithm aims to minimize the total differences of | |
43 | + matching players' rates. It also includes penalyties when a match | |
44 | + is same as the previous one or a match is between human players. | |
45 | + It is based on a discussion with Yamashita-san on | |
46 | + http://www.sgtpepper.net/kaneko/diary/20120511.html. | |
47 | + | |
48 | +2013-02-23 Daigo Moriwaki <daigo at debian dot org> | |
49 | + | |
50 | + * [shogi-server] | |
51 | + - New command: %%FORK <source_game> <new_buoy_game> [<nth-move>] | |
52 | + Fork a new game from the posistion where the n-th (starting from | |
53 | + one) move of a source game is played. The new game should be a | |
54 | + valid buoy game name. The default value of n is the position | |
55 | + where the previous position of the last one. | |
56 | + - The objective of this command: The shogi-server may be used as | |
57 | + the back end server of computer-human match where a human player | |
58 | + plays with a real board and someone, or a proxy, inputs moves to | |
59 | + the shogi-server. If the proxy happens to enter a wrong move, | |
60 | + with this command you can restart a new buoy game from the | |
61 | + previous stable position. | |
62 | + ex. %%FORK server-denou-14400-60+p1+p2+20130223185013 buoy_denou-14400-60 | |
63 | + | |
64 | +2012-12-30 Daigo Moriwaki <daigo at debian dot org> | |
65 | + | |
66 | + * [shogi-server] | |
67 | + - Backported a5c94012656902e73e00f46e7a4c7004b24d4578: | |
68 | + test/TC_logger.rb depeneded on a specific directory where it was | |
69 | + running on. This issues has been fixed. | |
70 | + - Backported 87d145bd1f1a14a33f5f6fbc78b63a1952f1ca90 and | |
71 | + 2df8c798aeb7f0e77735e893fd1370c2c6f15c4d: | |
72 | + shogi_server/floodgate.rb: Generating next time around the new | |
73 | + year day by reading configuration files did not work correctly. | |
74 | + This issue has been fixed. | |
75 | + | |
1 | 76 | 2012-12-28 Daigo Moriwaki <daigo at debian dot org> |
2 | 77 | |
3 | 78 | * [shogi-server] |
@@ -89,6 +89,7 @@ def grep(file) | ||
89 | 89 | puts [time, state, black_mark, black_id, white_id, white_mark, file].join("\t") |
90 | 90 | end |
91 | 91 | end |
92 | + $stdout.flush | |
92 | 93 | end |
93 | 94 | |
94 | 95 | # Show Usage |
@@ -49,6 +49,10 @@ | ||
49 | 49 | # m [days] (default 7) |
50 | 50 | # after m days, the half-life effect works |
51 | 51 | # |
52 | +# --ignore:: | |
53 | +# m [days] (default 365*2) | |
54 | +# old results will be ignored | |
55 | +# | |
52 | 56 | # --fixed-rate-player:: |
53 | 57 | # player whose rate is fixed at the rate |
54 | 58 | # |
@@ -660,6 +664,11 @@ def parse(line) | ||
660 | 664 | return if state == "abnormal" |
661 | 665 | time = Time.parse(time) |
662 | 666 | return if $options["base-date"] < time |
667 | + how_long_days = ($options["base-date"] - time)/(3600*24) | |
668 | + if (how_long_days > $options["ignore"]) | |
669 | + return | |
670 | + end | |
671 | + | |
663 | 672 | black_id = identify_id(black_id) |
664 | 673 | white_id = identify_id(white_id) |
665 | 674 |
@@ -697,6 +706,8 @@ OPTOINS: | ||
697 | 706 | --half-life n [days] (default 60) |
698 | 707 | --half-life-ignore m [days] (default 7) |
699 | 708 | after m days, half-life effect works |
709 | + --ignore n [days] (default 730 [=365*2]). | |
710 | + Results older than n days from the 'base-date' are ignored. | |
700 | 711 | --fixed-rate-player player whose rate is fixed at the rate |
701 | 712 | --fixed-rate rate |
702 | 713 | --skip-draw-games skip draw games. [default: draw games are counted in |
@@ -712,6 +723,7 @@ def main | ||
712 | 723 | ["--half-life", GetoptLong::REQUIRED_ARGUMENT], |
713 | 724 | ["--half-life-ignore", GetoptLong::REQUIRED_ARGUMENT], |
714 | 725 | ["--help", "-h", GetoptLong::NO_ARGUMENT], |
726 | + ["--ignore", GetoptLong::REQUIRED_ARGUMENT], | |
715 | 727 | ["--fixed-rate-player", GetoptLong::REQUIRED_ARGUMENT], |
716 | 728 | ["--fixed-rate", GetoptLong::REQUIRED_ARGUMENT], |
717 | 729 | ["--skip-draw-games", GetoptLong::NO_ARGUMENT]) |
@@ -744,6 +756,8 @@ def main | ||
744 | 756 | $options["half-life"] = $options["half-life"].to_i |
745 | 757 | $options["half-life-ignore"] ||= 7 |
746 | 758 | $options["half-life-ignore"] = $options["half-life-ignore"].to_i |
759 | + $options["ignore"] ||= 365*2 | |
760 | + $options["ignore"] = $options["ignore"].to_i | |
747 | 761 | $options["fixed-rate"] = $options["fixed-rate"].to_i if $options["fixed-rate"] |
748 | 762 | |
749 | 763 | if ARGV.empty? |
@@ -0,0 +1,796 @@ | ||
1 | +#!/usr/bin/ruby | |
2 | +# $Id: mk_rate 316 2008-12-28 15:10:10Z beatles $ | |
3 | +# | |
4 | +# Author:: Daigo Moriwaki | |
5 | +# Homepage:: http://sourceforge.jp/projects/shogi-server/ | |
6 | +# | |
7 | +#-- | |
8 | +# Copyright (C) 2006-2008 Daigo Moriwaki <daigo at debian dot org> | |
9 | +# | |
10 | +# This program is free software; you can redistribute it and/or modify | |
11 | +# it under the terms of the GNU General Public License as published by | |
12 | +# the Free Software Foundation; either version 2 of the License, or | |
13 | +# (at your option) any later version. | |
14 | +# | |
15 | +# This program is distributed in the hope that it will be useful, | |
16 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
17 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
18 | +# GNU General Public License for more details. | |
19 | +# | |
20 | +# You should have received a copy of the GNU General Public License | |
21 | +# along with this program; if not, write to the Free Software | |
22 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
23 | +#++ | |
24 | +# | |
25 | +# == Synopsis | |
26 | +# | |
27 | +# mk_rate reads CSA files, calculates rating scores of each player, and then | |
28 | +# outputs a yaml file (players.yaml) that Shogi-server can recognize. | |
29 | +# | |
30 | +# == Usage | |
31 | +# | |
32 | +# ./mk_rate [options] DIR.. | |
33 | +# | |
34 | +# DIR:: | |
35 | +# CSA files are recursively looked up the directories. | |
36 | +# | |
37 | +# --half-life:: | |
38 | +# n [days] (default 60) | |
39 | +# | |
40 | +# --half-life-ignore:: | |
41 | +# m [days] (default 7) | |
42 | +# after m days, the half-life effect works | |
43 | +# | |
44 | +# --ignore:: | |
45 | +# m [days] (default 365*2) | |
46 | +# old files will be ignored | |
47 | +# | |
48 | +# --fixed-rate-player:: | |
49 | +# player whose rate is fixed at the rate | |
50 | +# | |
51 | +# --fixed-rate:: | |
52 | +# rate | |
53 | +# | |
54 | +# --help:: | |
55 | +# show this message | |
56 | +# | |
57 | +# == PREREQUIRE | |
58 | +# | |
59 | +# Sample Command lines that isntall prerequires will work on Debian. | |
60 | +# | |
61 | +# * Ruby 1.8.7 | |
62 | +# | |
63 | +# $ sudo aptitude install ruby1.8 | |
64 | +# | |
65 | +# * Rubygems | |
66 | +# | |
67 | +# $ sudo aptitude install rubygems | |
68 | +# | |
69 | +# * Ruby bindings for the GNU Scientific Library (GSL[http://rb-gsl.rubyforge.org/]) | |
70 | +# | |
71 | +# $ sudo aptitude install libgsl-ruby1.8 | |
72 | +# | |
73 | +# * RGL: {Ruby Graph Library}[http://rubyforge.org/projects/rgl/] | |
74 | +# | |
75 | +# $ sudo gem install rgl | |
76 | +# | |
77 | +# == Run | |
78 | +# | |
79 | +# $ ./mk_rate . > players.yaml | |
80 | +# | |
81 | +# or, if you do not want the file to be update in case of errors, | |
82 | +# | |
83 | +# $ ./mk_rate . && ./mk_rate . > players.yaml | |
84 | +# | |
85 | +# == How players are rated | |
86 | +# | |
87 | +# The conditions that games and players are rated as following: | |
88 | +# | |
89 | +# * Rated games, which were played by both rated players. | |
90 | +# * Rated players, who logged in the server with a name followed by a trip: "name,trip". | |
91 | +# * (Rated) players, who played more than $GAMES_LIMIT [15] (rated) games. | |
92 | +# | |
93 | + | |
94 | +require 'yaml' | |
95 | +require 'time' | |
96 | +require 'getoptlong' | |
97 | +require 'gsl' | |
98 | +require 'rubygems' | |
99 | +require 'rgl/adjacency' | |
100 | +require 'rgl/connected_components' | |
101 | + | |
102 | +################################################# | |
103 | +# Constants | |
104 | +# | |
105 | + | |
106 | +# Count out players who play less games than $GAMES_LIMIT | |
107 | +$GAMES_LIMIT = $DEBUG ? 0 : 15 | |
108 | +WIN_MARK = "win" | |
109 | +LOSS_MARK = "lose" | |
110 | +DRAW_MARK = "draw" | |
111 | + | |
112 | +# Holds players | |
113 | +$players = Hash.new | |
114 | +# Holds the last time when a player gamed | |
115 | +$players_time = Hash.new { Time.at(0) } | |
116 | + | |
117 | + | |
118 | +################################################# | |
119 | +# Keeps the value of the lowest key | |
120 | +# | |
121 | +class Record | |
122 | + def initialize | |
123 | + @lowest = [] | |
124 | + end | |
125 | + | |
126 | + def set(key, value) | |
127 | + if @lowest.empty? || key < @lowest[0] | |
128 | + @lowest = [key, value] | |
129 | + end | |
130 | + end | |
131 | + | |
132 | + def get | |
133 | + if @lowest.empty? | |
134 | + nil | |
135 | + else | |
136 | + @lowest[1] | |
137 | + end | |
138 | + end | |
139 | +end | |
140 | + | |
141 | +################################################# | |
142 | +# Calculates rates of every player from a Win Loss GSL::Matrix | |
143 | +# | |
144 | +class Rating | |
145 | + include Math | |
146 | + | |
147 | + # The model of the win possibility is 1/(1 + 10^(-d/400)). | |
148 | + # The equation in this class is 1/(1 + e^(-Kd)). | |
149 | + # So, K should be calculated like this. | |
150 | + K = Math.log(10.0) / 400.0 | |
151 | + | |
152 | + # Convergence limit to stop Newton method. | |
153 | + ERROR_LIMIT = 1.0e-3 | |
154 | + # Stop Newton method after this iterations. | |
155 | + COUNT_MAX = 500 | |
156 | + | |
157 | + # Average rate among the players | |
158 | + AVERAGE_RATE = 1000 | |
159 | + | |
160 | + | |
161 | + ############### | |
162 | + # Class methods | |
163 | + # | |
164 | + | |
165 | + ## | |
166 | + # Calcurates the average of the vector. | |
167 | + # | |
168 | + def Rating.average(vector, mean=0.0) | |
169 | + sum = Array(vector).inject(0.0) {|sum, n| sum + n} | |
170 | + vector -= GSL::Vector[*Array.new(vector.size, sum/vector.size - mean)] | |
171 | + vector | |
172 | + end | |
173 | + | |
174 | + ################## | |
175 | + # Instance methods | |
176 | + # | |
177 | + def initialize(win_loss_matrix) | |
178 | + @record = Record.new | |
179 | + @n = win_loss_matrix | |
180 | + case @n | |
181 | + when GSL::Matrix, GSL::Matrix::Int | |
182 | + @size = @n.size1 | |
183 | + when ::Matrix | |
184 | + @size = @n.row_size | |
185 | + else | |
186 | + raise ArgumentError | |
187 | + end | |
188 | + initial_rate | |
189 | + end | |
190 | + attr_reader :rate, :n | |
191 | + | |
192 | + def player_vector | |
193 | + GSL::Vector[* | |
194 | + (0...@size).collect {|k| yield k} | |
195 | + ] | |
196 | + end | |
197 | + | |
198 | + def each_player | |
199 | + (0...@size).each {|k| yield k} | |
200 | + end | |
201 | + | |
202 | + ## | |
203 | + # The possibility that the player k will beet the player i. | |
204 | + # | |
205 | + def win_rate(k,i) | |
206 | + 1.0/(1.0 + exp(@rate[i]-@rate[k])) | |
207 | + end | |
208 | + | |
209 | + ## | |
210 | + # Most possible equation | |
211 | + # | |
212 | + def func_vector | |
213 | + player_vector do|k| | |
214 | + sum = 0.0 | |
215 | + each_player do |i| | |
216 | + next if i == k | |
217 | + sum += @n[k,i] * win_rate(i,k) - @n[i,k] * win_rate(k,i) | |
218 | + end | |
219 | + sum * 2.0 | |
220 | + end | |
221 | + end | |
222 | + | |
223 | + ## | |
224 | + # / f0/R0 f0/R1 f0/R2 ... \ | |
225 | + # dfk/dRj = | f1/R0 f1/R1 f1/R2 ... | | |
226 | + # \ f2/R0 f2/R1 f2/R2 ... / | |
227 | + def d_func(k,j) | |
228 | + sum = 0.0 | |
229 | + if k == j | |
230 | + each_player do |i| | |
231 | + next if i == k | |
232 | + sum += win_rate(i,k) * win_rate(k,i) * (@n[k,i] + @n[i,k]) | |
233 | + end | |
234 | + sum *= -2.0 | |
235 | + else # k != j | |
236 | + sum = 2.0 * win_rate(j,k) * win_rate(k,j) * (@n[k,j] + @n[j,k]) | |
237 | + end | |
238 | + sum | |
239 | + end | |
240 | + | |
241 | + ## | |
242 | + # Jacobi matrix of the func(). | |
243 | + # m00 m01 | |
244 | + # m10 m11 | |
245 | + # | |
246 | + def j_matrix | |
247 | + GSL::Matrix[* | |
248 | + (0...@size).collect do |k| | |
249 | + (0...@size).collect do |j| | |
250 | + d_func(k,j) | |
251 | + end | |
252 | + end | |
253 | + ] | |
254 | + end | |
255 | + | |
256 | + ## | |
257 | + # The initial value of the rate, which is of very importance for Newton | |
258 | + # method. This is based on my huristics; the higher the win probablity of | |
259 | + # a player is, the greater points he takes. | |
260 | + # | |
261 | + def initial_rate | |
262 | + possibility = | |
263 | + player_vector do |k| | |
264 | + v = GSL::Vector[0, 0] | |
265 | + each_player do |i| | |
266 | + next if k == i | |
267 | + v += GSL::Vector[@n[k,i], @n[i,k]] | |
268 | + end | |
269 | + v.nrm2 < 1 ? 0 : v[0] / (v[0] + v[1]) | |
270 | + end | |
271 | + rank = possibility.sort_index | |
272 | + @rate = player_vector do |k| | |
273 | + K*500 * (rank[k]+1) / @size | |
274 | + end | |
275 | + average! | |
276 | + end | |
277 | + | |
278 | + ## | |
279 | + # Resets @rate as the higher the current win probablity of a player is, | |
280 | + # the greater points he takes. | |
281 | + # | |
282 | + def initial_rate2 | |
283 | + @rate = @record.get || @rate | |
284 | + rank = @rate.sort_index | |
285 | + @rate = player_vector do |k| | |
286 | + K*@count*1.5 * (rank[k]+1) / @size | |
287 | + end | |
288 | + average! | |
289 | + end | |
290 | + | |
291 | + # mu is the deaccelrating parameter in Deaccelerated Newton method | |
292 | + def deaccelrate(mu, old_rate, a, old_f_nrm2) | |
293 | + @rate = old_rate - a * mu | |
294 | + if func_vector.nrm2 < (1 - mu / 4.0 ) * old_f_nrm2 then | |
295 | + return | |
296 | + end | |
297 | + if mu < 1e-4 | |
298 | + @record.set(func_vector.nrm2, @rate) | |
299 | + initial_rate2 | |
300 | + return | |
301 | + end | |
302 | + $stderr.puts "mu: %f " % [mu] if $DEBUG | |
303 | + deaccelrate(mu*0.5, old_rate, a, old_f_nrm2) | |
304 | + end | |
305 | + | |
306 | + ## | |
307 | + # Main process to calculate ratings. | |
308 | + # | |
309 | + def rating | |
310 | + # Counter to stop the process. | |
311 | + # Calulation in Newton method may fall in an infinite loop | |
312 | + @count = 0 | |
313 | + | |
314 | + # Main loop | |
315 | + begin | |
316 | + # Solve the equation: | |
317 | + # J*a=f | |
318 | + # @rate_(n+1) = @rate_(n) - a | |
319 | + # | |
320 | + # f.nrm2 should approach to zero. | |
321 | + f = func_vector | |
322 | + j = j_matrix | |
323 | + | |
324 | + # $stderr.puts "j: %s" % [j.inspect] if $DEBUG | |
325 | + $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2] if $DEBUG | |
326 | + | |
327 | + # GSL::Linalg::LU.solve or GSL::Linalg::HH.solve would be available instead. | |
328 | + #a = GSL::Linalg::HH.solve(j, f) | |
329 | + a, = GSL::MultiFit::linear(j, f) | |
330 | + a = self.class.average(a) | |
331 | + # $stderr.puts "a: %s -> %f" % [a.to_a.inspect, a.nrm2] if $DEBUG | |
332 | + | |
333 | + # Deaccelerated Newton method | |
334 | + # GSL::Vector object should be immutable. | |
335 | + old_rate = @rate | |
336 | + old_f = f | |
337 | + old_f_nrm2 = old_f.nrm2 | |
338 | + deaccelrate(1.0, old_rate, a, old_f_nrm2) | |
339 | + @record.set(func_vector.nrm2, @rate) | |
340 | + | |
341 | + $stderr.printf "|error| : %5.2e\n", a.nrm2 if $DEBUG | |
342 | + | |
343 | + @count += 1 | |
344 | + if @count > COUNT_MAX | |
345 | + $stderr.puts "Values seem to oscillate. Stopped the process." | |
346 | + $stderr.puts "f: %s -> %f" % [func_vector.to_a.inspect, func_vector.nrm2] | |
347 | + break | |
348 | + end | |
349 | + | |
350 | + end while (a.nrm2 > ERROR_LIMIT * @rate.nrm2) | |
351 | + | |
352 | + @rate = @record.get | |
353 | + $stderr.puts "resolved f: %s -> %f" % | |
354 | + [func_vector.to_a.inspect, func_vector.nrm2] if $DEBUG | |
355 | + | |
356 | + @rate *= 1.0/K | |
357 | + finite! | |
358 | + self | |
359 | + end | |
360 | + | |
361 | + ## | |
362 | + # Make the values of @rate finite. | |
363 | + # | |
364 | + def finite! | |
365 | + @rate = @rate.collect do |a| | |
366 | + if a.infinite? | |
367 | + a.infinite? * AVERAGE_RATE * 100 | |
368 | + else | |
369 | + a | |
370 | + end | |
371 | + end | |
372 | + end | |
373 | + | |
374 | + ## | |
375 | + # Flatten the values of @rate. | |
376 | + # | |
377 | + def average!(mean=0.0) | |
378 | + @rate = self.class.average(@rate, mean) | |
379 | + end | |
380 | + | |
381 | + ## | |
382 | + # Translate by value | |
383 | + # | |
384 | + def translate!(value) | |
385 | + @rate += value | |
386 | + end | |
387 | + | |
388 | + ## | |
389 | + # Make the values of @rate integer. | |
390 | + # | |
391 | + def integer! | |
392 | + @rate = @rate.collect do |a| | |
393 | + if a.finite? | |
394 | + a.to_i | |
395 | + elsif a.nan? | |
396 | + 0 | |
397 | + elsif a.infinite? | |
398 | + a.infinite? * AVERAGE_RATE * 100 | |
399 | + end | |
400 | + end | |
401 | + end | |
402 | +end | |
403 | + | |
404 | +################################################# | |
405 | +# Encapsulate a pair of keys and win loss matrix. | |
406 | +# - keys is an array of player IDs; [gps+123, foo+234, ...] | |
407 | +# - matrix holds games # where player i (row index) beats player j (column index). | |
408 | +# The row and column indexes match with the keys. | |
409 | +# | |
410 | +# This object should be immutable. If an internal state is being modified, a | |
411 | +# new object is always returned. | |
412 | +# | |
413 | +class WinLossMatrix | |
414 | + | |
415 | + ############### | |
416 | + # Class methods | |
417 | + # | |
418 | + | |
419 | + def self.mk_matrix(players) | |
420 | + keys = players.keys.sort | |
421 | + size = keys.size | |
422 | + matrix = | |
423 | + GSL::Matrix[* | |
424 | + ((0...size).collect do |k| | |
425 | + p1 = keys[k] | |
426 | + p1_hash = players[p1] | |
427 | + ((0...size).collect do |j| | |
428 | + if k == j | |
429 | + 0 | |
430 | + else | |
431 | + p2 = keys[j] | |
432 | + v = p1_hash[p2] || Vector[0,0] | |
433 | + v[0] | |
434 | + end | |
435 | + end) | |
436 | + end)] | |
437 | + return WinLossMatrix.new(keys, matrix) | |
438 | + end | |
439 | + | |
440 | + def self.mk_win_loss_matrix(players) | |
441 | + obj = mk_matrix(players) | |
442 | + return obj.filter | |
443 | + end | |
444 | + | |
445 | + ################## | |
446 | + # Instance methods | |
447 | + # | |
448 | + | |
449 | + # an array of player IDs; [gps+123, foo+234, ...] | |
450 | + attr_reader :keys | |
451 | + | |
452 | + # matrix holds games # where player i (row index) beats player j (column index). | |
453 | + # The row and column indexes match with the keys. | |
454 | + attr_reader :matrix | |
455 | + | |
456 | + def initialize(keys, matrix) | |
457 | + @keys = keys | |
458 | + @matrix = matrix | |
459 | + end | |
460 | + | |
461 | + ## | |
462 | + # Returns the size of the keys/matrix | |
463 | + # | |
464 | + def size | |
465 | + if @keys | |
466 | + @keys.size | |
467 | + else | |
468 | + nil | |
469 | + end | |
470 | + end | |
471 | + | |
472 | + ## | |
473 | + # Removes players in a rows such as [1,3,5], and then returns a new | |
474 | + # object. | |
475 | + # | |
476 | + def delete_rows(rows) | |
477 | + rows = rows.sort.reverse | |
478 | + | |
479 | + copied_cols = [] | |
480 | + (0...size).each do |i| | |
481 | + next if rows.include?(i) | |
482 | + row = @matrix.row(i).clone | |
483 | + rows.each do |j| | |
484 | + row.delete_at(j) | |
485 | + end | |
486 | + copied_cols << row | |
487 | + end | |
488 | + if copied_cols.size == 0 | |
489 | + new_matrix = GSL::Matrix.new | |
490 | + else | |
491 | + new_matrix = GSL::Matrix[*copied_cols] | |
492 | + end | |
493 | + | |
494 | + new_keys = @keys.clone | |
495 | + rows.each do |j| | |
496 | + new_keys.delete_at(j) | |
497 | + end | |
498 | + | |
499 | + return WinLossMatrix.new(new_keys, new_matrix) | |
500 | + end | |
501 | + | |
502 | + ## | |
503 | + # Removes players who do not pass a criteria to be rated, and returns a | |
504 | + # new object. | |
505 | + # | |
506 | + def filter | |
507 | + $stderr.puts @keys.inspect if $DEBUG | |
508 | + $stderr.puts @matrix.inspect if $DEBUG | |
509 | + delete = [] | |
510 | + (0...size).each do |i| | |
511 | + row = @matrix.row(i) | |
512 | + col = @matrix.col(i) | |
513 | + win = row.sum | |
514 | + loss = col.sum | |
515 | + if win < 1 || loss < 1 || win + loss < $GAMES_LIMIT | |
516 | + delete << i | |
517 | + end | |
518 | + end | |
519 | + | |
520 | + # The recursion ends if there is nothing to delete | |
521 | + return self if delete.empty? | |
522 | + | |
523 | + new_obj = delete_rows(delete) | |
524 | + new_obj.filter | |
525 | + end | |
526 | + | |
527 | + ## | |
528 | + # Cuts self into connecting groups such as each player in a group has at least | |
529 | + # one game with other players in the group. Returns them as an array. | |
530 | + # | |
531 | + def connected_subsets | |
532 | + g = RGL::AdjacencyGraph.new | |
533 | + (0...size).each do |k| | |
534 | + (0...size).each do |i| | |
535 | + next if k == i | |
536 | + if @matrix[k,i] > 0 | |
537 | + g.add_edge(k,i) | |
538 | + end | |
539 | + end | |
540 | + end | |
541 | + | |
542 | + subsets = [] | |
543 | + g.each_connected_component do |c| | |
544 | + new_keys = [] | |
545 | + c.each do |v| | |
546 | + new_keys << keys[v.to_s.to_i] | |
547 | + end | |
548 | + subsets << new_keys | |
549 | + end | |
550 | + | |
551 | + subsets = subsets.sort {|a,b| b.size <=> a.size} | |
552 | + | |
553 | + result = subsets.collect do |keys| | |
554 | + matrix = | |
555 | + GSL::Matrix[* | |
556 | + ((0...keys.size).collect do |k| | |
557 | + p1 = @keys.index(keys[k]) | |
558 | + ((0...keys.size).collect do |j| | |
559 | + if k == j | |
560 | + 0 | |
561 | + else | |
562 | + p2 = @keys.index(keys[j]) | |
563 | + @matrix[p1,p2] + 0.001 | |
564 | + end | |
565 | + end) | |
566 | + end)] | |
567 | + WinLossMatrix.new(keys, matrix) | |
568 | + end | |
569 | + | |
570 | + return result | |
571 | + end | |
572 | + | |
573 | + def to_s | |
574 | + "size : #{@keys.size}" + "\n" + | |
575 | + @keys.inspect + "\n" + | |
576 | + @matrix.inspect | |
577 | + end | |
578 | + | |
579 | +end | |
580 | + | |
581 | + | |
582 | +################################################# | |
583 | +# Main methods | |
584 | +# | |
585 | + | |
586 | +# Half-life effect | |
587 | +# After NHAFE_LIFE days value will get half. | |
588 | +# 0.693 is constant, where exp(0.693) ~ 0.5 | |
589 | +def half_life(days) | |
590 | + if days < $options["half-life-ignore"] | |
591 | + return 1.0 | |
592 | + else | |
593 | + Math::exp(-0.693/$options["half-life"]*(days-$options["half-life-ignore"])) | |
594 | + end | |
595 | +end | |
596 | + | |
597 | +def _add_win_loss(winner, loser, time) | |
598 | + how_long_days = (Time.now - time)/(3600*24) | |
599 | + $players[winner] ||= Hash.new { GSL::Vector[0,0] } | |
600 | + $players[loser] ||= Hash.new { GSL::Vector[0,0] } | |
601 | + $players[winner][loser] += GSL::Vector[1.0*half_life(how_long_days),0] | |
602 | + $players[loser][winner] += GSL::Vector[0,1.0*half_life(how_long_days)] | |
603 | +end | |
604 | + | |
605 | +def _add_time(player, time) | |
606 | + $players_time[player] = time if $players_time[player] < time | |
607 | +end | |
608 | + | |
609 | +def add(black_mark, black_name, white_name, white_mark, time) | |
610 | + how_long_days = (Time.now - time)/(3600*24) | |
611 | + if (how_long_days > $options["ignore"]) | |
612 | + return | |
613 | + end | |
614 | + if black_mark == WIN_MARK && white_mark == LOSS_MARK | |
615 | + _add_win_loss(black_name, white_name, time) | |
616 | + elsif black_mark == LOSS_MARK && white_mark == WIN_MARK | |
617 | + _add_win_loss(white_name, black_name, time) | |
618 | + elsif black_mark == DRAW_MARK && white_mark == DRAW_MARK | |
619 | + return | |
620 | + else | |
621 | + raise "Never reached!" | |
622 | + end | |
623 | + _add_time(black_name, time) | |
624 | + _add_time(white_name, time) | |
625 | +end | |
626 | + | |
627 | +def identify_id(id) | |
628 | + if /@NORATE\+/ =~ id # the player having @NORATE in the name should not be rated | |
629 | + return nil | |
630 | + end | |
631 | + id.gsub(/@.*?\+/,"+") | |
632 | +end | |
633 | + | |
634 | +def grep(str) | |
635 | + if /^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+)$/ =~ str.strip then | |
636 | + add($1,$2,$3,$4,Time.at($5.to_i)) | |
637 | + end | |
638 | +end | |
639 | + | |
640 | +def usage | |
641 | + $stderr.puts <<-EOF | |
642 | +USAGE: #{$0} dir [...] | |
643 | + EOF | |
644 | + exit 1 | |
645 | +end | |
646 | + | |
647 | +def validate(yaml) | |
648 | + yaml["players"].each do |group_key, group| | |
649 | + group.each do |player_key, player| | |
650 | + rate = player['rate'] | |
651 | + next unless rate | |
652 | + if rate > 10000 || rate < -10000 | |
653 | + return false | |
654 | + end | |
655 | + end | |
656 | + end | |
657 | + return true | |
658 | +end | |
659 | + | |
660 | +def usage(io) | |
661 | + io.puts <<EOF | |
662 | +USAGE: #{$0} [options] DIR.. | |
663 | + DIR where CSA files are looked up recursively | |
664 | +OPTOINS: | |
665 | + --half-life n [days] (default 60) | |
666 | + --half-life-ignore m [days] (default 7) | |
667 | + after m days, half-life effect works | |
668 | + --fixed-rate-player player whose rate is fixed at the rate | |
669 | + --fixed-rate rate | |
670 | + --help show this message | |
671 | +EOF | |
672 | +end | |
673 | + | |
674 | +def main | |
675 | + $options = Hash::new | |
676 | + parser = GetoptLong.new( | |
677 | + ["--half-life", GetoptLong::REQUIRED_ARGUMENT], | |
678 | + ["--half-life-ignore", GetoptLong::REQUIRED_ARGUMENT], | |
679 | + ["--ignore", GetoptLong::REQUIRED_ARGUMENT], | |
680 | + ["--help", "-h", GetoptLong::NO_ARGUMENT], | |
681 | + ["--fixed-rate-player", GetoptLong::REQUIRED_ARGUMENT], | |
682 | + ["--fixed-rate", GetoptLong::REQUIRED_ARGUMENT]) | |
683 | + parser.quiet = true | |
684 | + begin | |
685 | + parser.each_option do |name, arg| | |
686 | + name.sub!(/^--/, '') | |
687 | + $options[name] = arg.dup | |
688 | + end | |
689 | + if ( $options["fixed-rate-player"] && !$options["fixed-rate"]) || | |
690 | + (!$options["fixed-rate-player"] && $options["fixed-rate"]) || | |
691 | + ( $options["fixed-rate-player"] && $options["fixed-rate"].to_i <= 0) | |
692 | + usage($stderr) | |
693 | + exit 1 | |
694 | + end | |
695 | + rescue | |
696 | + usage($stderr) | |
697 | + raise parser.error_message | |
698 | + end | |
699 | + if $options["help"] | |
700 | + usage($stdout) | |
701 | + exit 0 | |
702 | + end | |
703 | + $options["half-life"] ||= 60 | |
704 | + $options["half-life"] = $options["half-life"].to_i | |
705 | + $options["half-life-ignore"] ||= 7 | |
706 | + $options["half-life-ignore"] = $options["half-life-ignore"].to_i | |
707 | + $options["ignore"] ||= 365*2 | |
708 | + $options["ignore"] = $options["ignore"].to_i | |
709 | + $options["fixed-rate"] = $options["fixed-rate"].to_i if $options["fixed-rate"] | |
710 | + | |
711 | + while line = $stdin.gets do | |
712 | + grep line.strip | |
713 | + end | |
714 | + | |
715 | + yaml = {} | |
716 | + yaml["players"] = {} | |
717 | + rating_group = 0 | |
718 | + if $players.size > 0 | |
719 | + obj = WinLossMatrix::mk_win_loss_matrix($players) | |
720 | + obj.connected_subsets.each do |win_loss_matrix| | |
721 | + yaml["players"][rating_group] = {} | |
722 | + | |
723 | + rating = Rating.new(win_loss_matrix.matrix) | |
724 | + rating.rating | |
725 | + rating.average!(Rating::AVERAGE_RATE) | |
726 | + rating.integer! | |
727 | + | |
728 | + if $options["fixed-rate-player"] | |
729 | + # first, try exact match | |
730 | + index = win_loss_matrix.keys.index($options["fixed-rate-player"]) | |
731 | + # second, try regular match | |
732 | + unless index | |
733 | + win_loss_matrix.keys.each_with_index do |p, i| | |
734 | + if %r!#{$options["fixed-rate-player"]}! =~ p | |
735 | + index = i | |
736 | + end | |
737 | + end | |
738 | + end | |
739 | + if index | |
740 | + the_rate = rating.rate[index] | |
741 | + rating.translate!($options["fixed-rate"] - the_rate) | |
742 | + end | |
743 | + end | |
744 | + | |
745 | + win_loss_matrix.keys.each_with_index do |p, i| # player_id, index# | |
746 | + win = win_loss_matrix.matrix.row(i).sum | |
747 | + loss = win_loss_matrix.matrix.col(i).sum | |
748 | + | |
749 | + yaml["players"][rating_group][p] = | |
750 | + { 'name' => p.split("+")[0], | |
751 | + 'rating_group' => rating_group, | |
752 | + 'rate' => rating.rate[i], | |
753 | + 'last_modified' => $players_time[p].dup, | |
754 | + 'win' => win, | |
755 | + 'loss' => loss} | |
756 | + end | |
757 | + rating_group += 1 | |
758 | + end | |
759 | + end | |
760 | + rating_group -= 1 | |
761 | + non_rated_group = 999 # large enough | |
762 | + yaml["players"][non_rated_group] = {} | |
763 | + $players.each_key do |id| | |
764 | + # skip players who have already been rated | |
765 | + found = false | |
766 | + (0..rating_group).each do |i| | |
767 | + found = true if yaml["players"][i][id] | |
768 | + break if found | |
769 | + end | |
770 | + next if found | |
771 | + | |
772 | + v = GSL::Vector[0, 0] | |
773 | + $players[id].each_value {|value| v += value} | |
774 | + next if v[0] < 1 && v[1] < 1 | |
775 | + | |
776 | + yaml["players"][non_rated_group][id] = | |
777 | + { 'name' => id.split("+")[0], | |
778 | + 'rating_group' => non_rated_group, | |
779 | + 'rate' => 0, | |
780 | + 'last_modified' => $players_time[id].dup, | |
781 | + 'win' => v[0], | |
782 | + 'loss' => v[1]} | |
783 | + end | |
784 | + unless validate(yaml) | |
785 | + $stderr.puts "Aborted. It did not result in valid ratings." | |
786 | + $stderr.puts yaml.to_yaml if $DEBUG | |
787 | + exit 10 | |
788 | + end | |
789 | + puts yaml.to_yaml | |
790 | +end | |
791 | + | |
792 | +if __FILE__ == $0 | |
793 | + main | |
794 | +end | |
795 | + | |
796 | +# vim: ts=2 sw=2 sts=0 |
@@ -0,0 +1,747 @@ | ||
1 | +#!/usr/bin/ruby | |
2 | +# $Id: mk_rate 316 2008-12-28 15:10:10Z beatles $ | |
3 | +# | |
4 | +# Author:: Daigo Moriwaki | |
5 | +# Homepage:: http://sourceforge.jp/projects/shogi-server/ | |
6 | +# | |
7 | +#-- | |
8 | +# Copyright (C) 2006-2008 Daigo Moriwaki <daigo at debian dot org> | |
9 | +# | |
10 | +# This program is free software; you can redistribute it and/or modify | |
11 | +# it under the terms of the GNU General Public License as published by | |
12 | +# the Free Software Foundation; either version 2 of the License, or | |
13 | +# (at your option) any later version. | |
14 | +# | |
15 | +# This program is distributed in the hope that it will be useful, | |
16 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
17 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
18 | +# GNU General Public License for more details. | |
19 | +# | |
20 | +# You should have received a copy of the GNU General Public License | |
21 | +# along with this program; if not, write to the Free Software | |
22 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
23 | +#++ | |
24 | +# | |
25 | +# == Synopsis | |
26 | +# | |
27 | +# mk_rate reads CSA files, calculates rating scores of each player, and then | |
28 | +# outputs a yaml file (players.yaml) that Shogi-server can recognize. | |
29 | +# | |
30 | +# == Usage | |
31 | +# | |
32 | +# ./mk_rate [options] DIR.. | |
33 | +# | |
34 | +# DIR:: | |
35 | +# CSA files are recursively looked up the directories. | |
36 | +# | |
37 | +# --half-life:: | |
38 | +# n [days] (default 60) | |
39 | +# | |
40 | +# --half-life-ignore:: | |
41 | +# m [days] (default 7) | |
42 | +# after m days, the half-life effect works | |
43 | +# | |
44 | +# --fixed-rate-player:: | |
45 | +# player whose rate is fixed at the rate | |
46 | +# | |
47 | +# --fixed-rate:: | |
48 | +# rate | |
49 | +# | |
50 | +# --help:: | |
51 | +# show this message | |
52 | +# | |
53 | +# == PREREQUIRE | |
54 | +# | |
55 | +# Sample Command lines that isntall prerequires will work on Debian. | |
56 | +# | |
57 | +# * Ruby 1.8.7 | |
58 | +# | |
59 | +# $ sudo aptitude install ruby1.8 | |
60 | +# | |
61 | +# * Rubygems | |
62 | +# | |
63 | +# $ sudo aptitude install rubygems | |
64 | +# | |
65 | +# * Ruby bindings for the GNU Scientific Library (GSL[http://rb-gsl.rubyforge.org/]) | |
66 | +# | |
67 | +# $ sudo aptitude install libgsl-ruby1.8 | |
68 | +# | |
69 | +# * RGL: {Ruby Graph Library}[http://rubyforge.org/projects/rgl/] | |
70 | +# | |
71 | +# $ sudo gem install rgl | |
72 | +# | |
73 | +# == Run | |
74 | +# | |
75 | +# $ ./mk_rate . > players.yaml | |
76 | +# | |
77 | +# or, if you do not want the file to be update in case of errors, | |
78 | +# | |
79 | +# $ ./mk_rate . && ./mk_rate . > players.yaml | |
80 | +# | |
81 | +# == How players are rated | |
82 | +# | |
83 | +# The conditions that games and players are rated as following: | |
84 | +# | |
85 | +# * Rated games, which were played by both rated players. | |
86 | +# * Rated players, who logged in the server with a name followed by a trip: "name,trip". | |
87 | +# * (Rated) players, who played more than $GAMES_LIMIT [15] (rated) games. | |
88 | +# | |
89 | + | |
90 | +require 'yaml' | |
91 | +require 'time' | |
92 | +require 'getoptlong' | |
93 | +require 'gsl' | |
94 | +require 'rubygems' | |
95 | +require 'rgl/adjacency' | |
96 | +require 'rgl/connected_components' | |
97 | + | |
98 | +################################################# | |
99 | +# Constants | |
100 | +# | |
101 | + | |
102 | +# Count out players who play less games than $GAMES_LIMIT | |
103 | +$GAMES_LIMIT = $DEBUG ? 0 : 15 | |
104 | +WIN_MARK = "win" | |
105 | +LOSS_MARK = "lose" | |
106 | +DRAW_MARK = "draw" | |
107 | + | |
108 | +# Holds players | |
109 | +$players = Hash.new | |
110 | +# Holds the last time when a player gamed | |
111 | +$players_time = Hash.new { Time.at(0) } | |
112 | + | |
113 | + | |
114 | +################################################# | |
115 | +# Keeps the value of the lowest key | |
116 | +# | |
117 | +class Record | |
118 | + def initialize | |
119 | + @lowest = [] | |
120 | + end | |
121 | + | |
122 | + def set(key, value) | |
123 | + if @lowest.empty? || key < @lowest[0] | |
124 | + @lowest = [key, value] | |
125 | + end | |
126 | + end | |
127 | + | |
128 | + def get | |
129 | + if @lowest.empty? | |
130 | + nil | |
131 | + else | |
132 | + @lowest[1] | |
133 | + end | |
134 | + end | |
135 | +end | |
136 | + | |
137 | +################################################# | |
138 | +# Calculates rates of every player from a Win Loss GSL::Matrix | |
139 | +# | |
140 | +class Rating | |
141 | + include Math | |
142 | + | |
143 | + # The model of the win possibility is 1/(1 + 10^(-d/400)). | |
144 | + # The equation in this class is 1/(1 + e^(-Kd)). | |
145 | + # So, K should be calculated like this. | |
146 | + K = Math.log(10.0) / 400.0 | |
147 | + | |
148 | + # Convergence limit to stop Newton method. | |
149 | + ERROR_LIMIT = 1.0e-3 | |
150 | + # Stop Newton method after this iterations. | |
151 | + COUNT_MAX = 500 | |
152 | + | |
153 | + # Average rate among the players | |
154 | + AVERAGE_RATE = 1000 | |
155 | + | |
156 | + | |
157 | + ############### | |
158 | + # Class methods | |
159 | + # | |
160 | + | |
161 | + ## | |
162 | + # Calcurates the average of the vector. | |
163 | + # | |
164 | + def Rating.average(vector, mean=0.0) | |
165 | + sum = Array(vector).inject(0.0) {|sum, n| sum + n} | |
166 | + vector -= GSL::Vector[*Array.new(vector.size, sum/vector.size - mean)] | |
167 | + vector | |
168 | + end | |
169 | + | |
170 | + ################## | |
171 | + # Instance methods | |
172 | + # | |
173 | + def initialize(win_loss_matrix) | |
174 | + @record = Record.new | |
175 | + @n = win_loss_matrix | |
176 | + case @n | |
177 | + when GSL::Matrix, GSL::Matrix::Int | |
178 | + @size = @n.size1 | |
179 | + when ::Matrix | |
180 | + @size = @n.row_size | |
181 | + else | |
182 | + raise ArgumentError | |
183 | + end | |
184 | + initial_rate | |
185 | + end | |
186 | + attr_reader :rate, :n | |
187 | + | |
188 | + def player_vector | |
189 | + GSL::Vector[* | |
190 | + (0...@size).collect {|k| yield k} | |
191 | + ] | |
192 | + end | |
193 | + | |
194 | + def each_player | |
195 | + (0...@size).each {|k| yield k} | |
196 | + end | |
197 | + | |
198 | + ## | |
199 | + # The possibility that the player k will beet the player i. | |
200 | + # | |
201 | + def win_rate(k,i) | |
202 | + 1.0/(1.0 + exp(@rate[i]-@rate[k])) | |
203 | + end | |
204 | + | |
205 | + ## | |
206 | + # Most possible equation | |
207 | + # | |
208 | + def func_vector | |
209 | + player_vector do|k| | |
210 | + sum = 0.0 | |
211 | + each_player do |i| | |
212 | + next if i == k | |
213 | + sum += @n[k,i] * win_rate(i,k) - @n[i,k] * win_rate(k,i) | |
214 | + end | |
215 | + sum * 2.0 | |
216 | + end | |
217 | + end | |
218 | + | |
219 | + ## | |
220 | + # / f0/R0 f0/R1 f0/R2 ... \ | |
221 | + # dfk/dRj = | f1/R0 f1/R1 f1/R2 ... | | |
222 | + # \ f2/R0 f2/R1 f2/R2 ... / | |
223 | + def d_func(k,j) | |
224 | + sum = 0.0 | |
225 | + if k == j | |
226 | + each_player do |i| | |
227 | + next if i == k | |
228 | + sum += win_rate(i,k) * win_rate(k,i) * (@n[k,i] + @n[i,k]) | |
229 | + end | |
230 | + sum *= -2.0 | |
231 | + else # k != j | |
232 | + sum = 2.0 * win_rate(j,k) * win_rate(k,j) * (@n[k,j] + @n[j,k]) | |
233 | + end | |
234 | + sum | |
235 | + end | |
236 | + | |
237 | + ## | |
238 | + # Jacobi matrix of the func(). | |
239 | + # m00 m01 | |
240 | + # m10 m11 | |
241 | + # | |
242 | + def j_matrix | |
243 | + GSL::Matrix[* | |
244 | + (0...@size).collect do |k| | |
245 | + (0...@size).collect do |j| | |
246 | + d_func(k,j) | |
247 | + end | |
248 | + end | |
249 | + ] | |
250 | + end | |
251 | + | |
252 | + ## | |
253 | + # The initial value of the rate, which is of very importance for Newton | |
254 | + # method. This is based on my huristics; the higher the win probablity of | |
255 | + # a player is, the greater points he takes. | |
256 | + # | |
257 | + def initial_rate | |
258 | + possibility = | |
259 | + player_vector do |k| | |
260 | + v = GSL::Vector[0, 0] | |
261 | + each_player do |i| | |
262 | + next if k == i | |
263 | + v += GSL::Vector[@n[k,i], @n[i,k]] | |
264 | + end | |
265 | + v.nrm2 < 1 ? 0 : v[0] / (v[0] + v[1]) | |
266 | + end | |
267 | + rank = possibility.sort_index | |
268 | + @rate = player_vector do |k| | |
269 | + K*500 * (rank[k]+1) / @size | |
270 | + end | |
271 | + average! | |
272 | + end | |
273 | + | |
274 | + ## | |
275 | + # Resets @rate as the higher the current win probablity of a player is, | |
276 | + # the greater points he takes. | |
277 | + # | |
278 | + def initial_rate2 | |
279 | + @rate = @record.get || @rate | |
280 | + rank = @rate.sort_index | |
281 | + @rate = player_vector do |k| | |
282 | + K*@count*1.5 * (rank[k]+1) / @size | |
283 | + end | |
284 | + average! | |
285 | + end | |
286 | + | |
287 | + # mu is the deaccelrating parameter in Deaccelerated Newton method | |
288 | + def deaccelrate(mu, old_rate, a, old_f_nrm2) | |
289 | + @rate = old_rate - a * mu | |
290 | + if func_vector.nrm2 < (1 - mu / 4.0 ) * old_f_nrm2 then | |
291 | + return | |
292 | + end | |
293 | + if mu < 1e-4 | |
294 | + @record.set(func_vector.nrm2, @rate) | |
295 | + initial_rate2 | |
296 | + return | |
297 | + end | |
298 | + $stderr.puts "mu: %f " % [mu] if $DEBUG | |
299 | + deaccelrate(mu*0.5, old_rate, a, old_f_nrm2) | |
300 | + end | |
301 | + | |
302 | + ## | |
303 | + # Main process to calculate ratings. | |
304 | + # | |
305 | + def rating | |
306 | + # Counter to stop the process. | |
307 | + # Calulation in Newton method may fall in an infinite loop | |
308 | + @count = 0 | |
309 | + | |
310 | + # Main loop | |
311 | + begin | |
312 | + # Solve the equation: | |
313 | + # J*a=f | |
314 | + # @rate_(n+1) = @rate_(n) - a | |
315 | + # | |
316 | + # f.nrm2 should approach to zero. | |
317 | + f = func_vector | |
318 | + j = j_matrix | |
319 | + | |
320 | + # $stderr.puts "j: %s" % [j.inspect] if $DEBUG | |
321 | + $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2] if $DEBUG | |
322 | + | |
323 | + # GSL::Linalg::LU.solve or GSL::Linalg::HH.solve would be available instead. | |
324 | + #a = GSL::Linalg::HH.solve(j, f) | |
325 | + a, = GSL::MultiFit::linear(j, f) | |
326 | + a = self.class.average(a) | |
327 | + # $stderr.puts "a: %s -> %f" % [a.to_a.inspect, a.nrm2] if $DEBUG | |
328 | + | |
329 | + # Deaccelerated Newton method | |
330 | + # GSL::Vector object should be immutable. | |
331 | + old_rate = @rate | |
332 | + old_f = f | |
333 | + old_f_nrm2 = old_f.nrm2 | |
334 | + deaccelrate(1.0, old_rate, a, old_f_nrm2) | |
335 | + @record.set(func_vector.nrm2, @rate) | |
336 | + | |
337 | + $stderr.printf "|error| : %5.2e\n", a.nrm2 if $DEBUG | |
338 | + | |
339 | + @count += 1 | |
340 | + if @count > COUNT_MAX | |
341 | + $stderr.puts "Values seem to oscillate. Stopped the process." | |
342 | + $stderr.puts "f: %s -> %f" % [func_vector.to_a.inspect, func_vector.nrm2] | |
343 | + break | |
344 | + end | |
345 | + | |
346 | + end while (a.nrm2 > ERROR_LIMIT * @rate.nrm2) | |
347 | + | |
348 | + @rate = @record.get | |
349 | + $stderr.puts "resolved f: %s -> %f" % | |
350 | + [func_vector.to_a.inspect, func_vector.nrm2] if $DEBUG | |
351 | + | |
352 | + @rate *= 1.0/K | |
353 | + finite! | |
354 | + self | |
355 | + end | |
356 | + | |
357 | + ## | |
358 | + # Make the values of @rate finite. | |
359 | + # | |
360 | + def finite! | |
361 | + @rate = @rate.collect do |a| | |
362 | + if a.infinite? | |
363 | + a.infinite? * AVERAGE_RATE * 100 | |
364 | + else | |
365 | + a | |
366 | + end | |
367 | + end | |
368 | + end | |
369 | + | |
370 | + ## | |
371 | + # Flatten the values of @rate. | |
372 | + # | |
373 | + def average!(mean=0.0) | |
374 | + @rate = self.class.average(@rate, mean) | |
375 | + end | |
376 | + | |
377 | + ## | |
378 | + # Translate by value | |
379 | + # | |
380 | + def translate!(value) | |
381 | + @rate += value | |
382 | + end | |
383 | + | |
384 | + ## | |
385 | + # Make the values of @rate integer. | |
386 | + # | |
387 | + def integer! | |
388 | + @rate = @rate.collect do |a| | |
389 | + if a.finite? | |
390 | + a.to_i | |
391 | + elsif a.nan? | |
392 | + 0 | |
393 | + elsif a.infinite? | |
394 | + a.infinite? * AVERAGE_RATE * 100 | |
395 | + end | |
396 | + end | |
397 | + end | |
398 | +end | |
399 | + | |
400 | +################################################# | |
401 | +# Encapsulate a pair of keys and win loss matrix. | |
402 | +# - keys is an array of player IDs; [gps+123, foo+234, ...] | |
403 | +# - matrix holds games # where player i (row index) beats player j (column index). | |
404 | +# The row and column indexes match with the keys. | |
405 | +# | |
406 | +# This object should be immutable. If an internal state is being modified, a | |
407 | +# new object is always returned. | |
408 | +# | |
409 | +class WinLossMatrix | |
410 | + | |
411 | + ############### | |
412 | + # Class methods | |
413 | + # | |
414 | + | |
415 | + def self.mk_matrix(players) | |
416 | + keys = players.keys.sort | |
417 | + size = keys.size | |
418 | + matrix = | |
419 | + GSL::Matrix[* | |
420 | + ((0...size).collect do |k| | |
421 | + p1 = keys[k] | |
422 | + p1_hash = players[p1] | |
423 | + ((0...size).collect do |j| | |
424 | + if k == j | |
425 | + 0 | |
426 | + else | |
427 | + p2 = keys[j] | |
428 | + v = p1_hash[p2] || Vector[0,0] | |
429 | + v[0] | |
430 | + end | |
431 | + end) | |
432 | + end)] | |
433 | + return WinLossMatrix.new(keys, matrix) | |
434 | + end | |
435 | + | |
436 | + def self.mk_win_loss_matrix(players) | |
437 | + obj = mk_matrix(players) | |
438 | + return obj.filter | |
439 | + end | |
440 | + | |
441 | + ################## | |
442 | + # Instance methods | |
443 | + # | |
444 | + | |
445 | + # an array of player IDs; [gps+123, foo+234, ...] | |
446 | + attr_reader :keys | |
447 | + | |
448 | + # matrix holds games # where player i (row index) beats player j (column index). | |
449 | + # The row and column indexes match with the keys. | |
450 | + attr_reader :matrix | |
451 | + | |
452 | + def initialize(keys, matrix) | |
453 | + @keys = keys | |
454 | + @matrix = matrix | |
455 | + end | |
456 | + | |
457 | + ## | |
458 | + # Returns the size of the keys/matrix | |
459 | + # | |
460 | + def size | |
461 | + if @keys | |
462 | + @keys.size | |
463 | + else | |
464 | + nil | |
465 | + end | |
466 | + end | |
467 | + | |
468 | + ## | |
469 | + # Removes players in a rows such as [1,3,5], and then returns a new | |
470 | + # object. | |
471 | + # | |
472 | + def delete_rows(rows) | |
473 | + rows = rows.sort.reverse | |
474 | + | |
475 | + copied_cols = [] | |
476 | + (0...size).each do |i| | |
477 | + next if rows.include?(i) | |
478 | + row = @matrix.row(i).clone | |
479 | + rows.each do |j| | |
480 | + row.delete_at(j) | |
481 | + end | |
482 | + copied_cols << row | |
483 | + end | |
484 | + if copied_cols.size == 0 | |
485 | + new_matrix = GSL::Matrix.new | |
486 | + else | |
487 | + new_matrix = GSL::Matrix[*copied_cols] | |
488 | + end | |
489 | + | |
490 | + new_keys = @keys.clone | |
491 | + rows.each do |j| | |
492 | + new_keys.delete_at(j) | |
493 | + end | |
494 | + | |
495 | + return WinLossMatrix.new(new_keys, new_matrix) | |
496 | + end | |
497 | + | |
498 | + ## | |
499 | + # Removes players who do not pass a criteria to be rated, and returns a | |
500 | + # new object. | |
501 | + # | |
502 | + def filter | |
503 | + $stderr.puts @keys.inspect if $DEBUG | |
504 | + $stderr.puts @matrix.inspect if $DEBUG | |
505 | + delete = [] | |
506 | + (0...size).each do |i| | |
507 | + row = @matrix.row(i) | |
508 | + col = @matrix.col(i) | |
509 | + win = row.sum | |
510 | + loss = col.sum | |
511 | + if win < 1 || loss < 1 || win + loss < $GAMES_LIMIT | |
512 | + delete << i | |
513 | + end | |
514 | + end | |
515 | + | |
516 | + # The recursion ends if there is nothing to delete | |
517 | + return self if delete.empty? | |
518 | + | |
519 | + new_obj = delete_rows(delete) | |
520 | + new_obj.filter | |
521 | + end | |
522 | + | |
523 | + ## | |
524 | + # Cuts self into connecting groups such as each player in a group has at least | |
525 | + # one game with other players in the group. Returns them as an array. | |
526 | + # | |
527 | + def connected_subsets | |
528 | + g = RGL::AdjacencyGraph.new | |
529 | + (0...size).each do |k| | |
530 | + (0...size).each do |i| | |
531 | + next if k == i | |
532 | + if @matrix[k,i] > 0 | |
533 | + g.add_edge(k,i) | |
534 | + end | |
535 | + end | |
536 | + end | |
537 | + | |
538 | + subsets = [] | |
539 | + g.each_connected_component do |c| | |
540 | + new_keys = [] | |
541 | + c.each do |v| | |
542 | + new_keys << keys[v.to_s.to_i] | |
543 | + end | |
544 | + subsets << new_keys | |
545 | + end | |
546 | + | |
547 | + subsets = subsets.sort {|a,b| b.size <=> a.size} | |
548 | + | |
549 | + result = subsets.collect do |keys| | |
550 | + matrix = | |
551 | + GSL::Matrix[* | |
552 | + ((0...keys.size).collect do |k| | |
553 | + p1 = @keys.index(keys[k]) | |
554 | + ((0...keys.size).collect do |j| | |
555 | + if k == j | |
556 | + 0 | |
557 | + else | |
558 | + p2 = @keys.index(keys[j]) | |
559 | + @matrix[p1,p2] | |
560 | + end | |
561 | + end) | |
562 | + end)] | |
563 | + WinLossMatrix.new(keys, matrix) | |
564 | + end | |
565 | + | |
566 | + return result | |
567 | + end | |
568 | + | |
569 | + def to_s | |
570 | + "size : #{@keys.size}" + "\n" + | |
571 | + @keys.inspect + "\n" + | |
572 | + @matrix.inspect | |
573 | + end | |
574 | + | |
575 | +end | |
576 | + | |
577 | + | |
578 | +################################################# | |
579 | +# Main methods | |
580 | +# | |
581 | + | |
582 | +# Half-life effect | |
583 | +# After NHAFE_LIFE days value will get half. | |
584 | +# 0.693 is constant, where exp(0.693) ~ 0.5 | |
585 | +def half_life(days) | |
586 | + if days < $options["half-life-ignore"] | |
587 | + return 1.0 | |
588 | + else | |
589 | + Math::exp(-0.693/$options["half-life"]*(days-$options["half-life-ignore"])) | |
590 | + end | |
591 | +end | |
592 | + | |
593 | +def _add_win_loss(winner, loser, time) | |
594 | + how_long_days = (Time.now - time)/(3600*24) | |
595 | + $players[winner] ||= Hash.new { GSL::Vector[0,0] } | |
596 | + $players[loser] ||= Hash.new { GSL::Vector[0,0] } | |
597 | + $players[winner][loser] += GSL::Vector[1.0*half_life(how_long_days),0] | |
598 | + $players[loser][winner] += GSL::Vector[0,1.0*half_life(how_long_days)] | |
599 | +end | |
600 | + | |
601 | +def _add_time(player, time) | |
602 | + $players_time[player] = time if $players_time[player] < time | |
603 | +end | |
604 | + | |
605 | +def add(black_mark, black_name, white_name, white_mark, time) | |
606 | + if black_mark == WIN_MARK && white_mark == LOSS_MARK | |
607 | + _add_win_loss(black_name, white_name, time) | |
608 | + elsif black_mark == LOSS_MARK && white_mark == WIN_MARK | |
609 | + _add_win_loss(white_name, black_name, time) | |
610 | + elsif black_mark == DRAW_MARK && white_mark == DRAW_MARK | |
611 | + return | |
612 | + else | |
613 | + raise "Never reached!" | |
614 | + end | |
615 | + _add_time(black_name, time) | |
616 | + _add_time(white_name, time) | |
617 | +end | |
618 | + | |
619 | +def identify_id(id) | |
620 | + if /@NORATE\+/ =~ id # the player having @NORATE in the name should not be rated | |
621 | + return nil | |
622 | + end | |
623 | + id.gsub(/@.*?\+/,"+") | |
624 | +end | |
625 | + | |
626 | +def grep(file) | |
627 | + str = File.open(file).read | |
628 | + | |
629 | + if /^N\+(.*)$/ =~ str then black_name = $1.strip end | |
630 | + if /^N\-(.*)$/ =~ str then white_name = $1.strip end | |
631 | + | |
632 | + if /^'summary:(.*)$/ =~ str | |
633 | + state, p1, p2 = $1.split(":").map {|a| a.strip} | |
634 | + return if state == "abnormal" | |
635 | + p1_name, p1_mark = p1.split(" ") | |
636 | + p2_name, p2_mark = p2.split(" ") | |
637 | + if p1_name == black_name | |
638 | + black_name, black_mark = p1_name, p1_mark | |
639 | + white_name, white_mark = p2_name, p2_mark | |
640 | + elsif p2_name == black_name | |
641 | + black_name, black_mark = p2_name, p2_mark | |
642 | + white_name, white_mark = p1_name, p1_mark | |
643 | + else | |
644 | + raise "Never reach!: #{black} #{white} #{p3} #{p2}" | |
645 | + end | |
646 | + end | |
647 | + if /^'\$END_TIME:(.*)$/ =~ str | |
648 | + time = Time.parse($1.strip) | |
649 | + end | |
650 | + if /^'rating:(.*)$/ =~ str | |
651 | + black_id, white_id = $1.split(":").map {|a| a.strip} | |
652 | + black_id = identify_id(black_id) | |
653 | + white_id = identify_id(white_id) | |
654 | + if black_id && white_id && (black_id != white_id) && | |
655 | + black_mark && white_mark | |
656 | + $stdout.printf("%s %s %s %s %d\n", black_mark, black_id, white_id, white_mark, time) | |
657 | + $stdout.flush | |
658 | + end | |
659 | + end | |
660 | +end | |
661 | + | |
662 | +def usage | |
663 | + $stderr.puts <<-EOF | |
664 | +USAGE: #{$0} dir [...] | |
665 | + EOF | |
666 | + exit 1 | |
667 | +end | |
668 | + | |
669 | +def validate(yaml) | |
670 | + yaml["players"].each do |group_key, group| | |
671 | + group.each do |player_key, player| | |
672 | + rate = player['rate'] | |
673 | + next unless rate | |
674 | + if rate > 10000 || rate < -10000 | |
675 | + return false | |
676 | + end | |
677 | + end | |
678 | + end | |
679 | + return true | |
680 | +end | |
681 | + | |
682 | +def usage(io) | |
683 | + io.puts <<EOF | |
684 | +USAGE: #{$0} [options] DIR.. | |
685 | + DIR where CSA files are looked up recursively | |
686 | +OPTOINS: | |
687 | + --half-life n [days] (default 60) | |
688 | + --half-life-ignore m [days] (default 7) | |
689 | + after m days, half-life effect works | |
690 | + --fixed-rate-player player whose rate is fixed at the rate | |
691 | + --fixed-rate rate | |
692 | + --help show this message | |
693 | +EOF | |
694 | +end | |
695 | + | |
696 | +def main | |
697 | + $options = Hash::new | |
698 | + parser = GetoptLong.new( | |
699 | + ["--half-life", GetoptLong::REQUIRED_ARGUMENT], | |
700 | + ["--half-life-ignore", GetoptLong::REQUIRED_ARGUMENT], | |
701 | + ["--help", "-h", GetoptLong::NO_ARGUMENT], | |
702 | + ["--fixed-rate-player", GetoptLong::REQUIRED_ARGUMENT], | |
703 | + ["--fixed-rate", GetoptLong::REQUIRED_ARGUMENT]) | |
704 | + parser.quiet = true | |
705 | + begin | |
706 | + parser.each_option do |name, arg| | |
707 | + name.sub!(/^--/, '') | |
708 | + $options[name] = arg.dup | |
709 | + end | |
710 | + if ( $options["fixed-rate-player"] && !$options["fixed-rate"]) || | |
711 | + (!$options["fixed-rate-player"] && $options["fixed-rate"]) || | |
712 | + ( $options["fixed-rate-player"] && $options["fixed-rate"].to_i <= 0) | |
713 | + usage($stderr) | |
714 | + exit 1 | |
715 | + end | |
716 | + rescue | |
717 | + usage($stderr) | |
718 | + raise parser.error_message | |
719 | + end | |
720 | + if $options["help"] | |
721 | + usage($stdout) | |
722 | + exit 0 | |
723 | + end | |
724 | + $options["half-life"] ||= 60 | |
725 | + $options["half-life"] = $options["half-life"].to_i | |
726 | + $options["half-life-ignore"] ||= 7 | |
727 | + $options["half-life-ignore"] = $options["half-life-ignore"].to_i | |
728 | + $options["fixed-rate"] = $options["fixed-rate"].to_i if $options["fixed-rate"] | |
729 | + | |
730 | + if ARGV.empty? | |
731 | + while line = $stdin.gets do | |
732 | + next unless %r!.*\.csa$! =~ line | |
733 | + grep line.strip | |
734 | + end | |
735 | + else | |
736 | + while dir = ARGV.shift do | |
737 | + Dir.glob( File.join(dir, "**", "*.csa") ) {|f| grep(f)} | |
738 | + end | |
739 | + end | |
740 | + $stderr.puts "read done." | |
741 | +end | |
742 | + | |
743 | +if __FILE__ == $0 | |
744 | + main | |
745 | +end | |
746 | + | |
747 | +# vim: ts=2 sw=2 sts=0 |
@@ -143,9 +143,6 @@ LICENSE | ||
143 | 143 | |
144 | 144 | SEE ALSO |
145 | 145 | |
146 | -RELEASE | |
147 | - #{ShogiServer::Release} | |
148 | - | |
149 | 146 | REVISION |
150 | 147 | #{ShogiServer::Revision} |
151 | 148 |
@@ -51,8 +51,7 @@ Default_Game_Name = "default-1500-0" | ||
51 | 51 | One_Time = 10 |
52 | 52 | Least_Time_Per_Move = 1 |
53 | 53 | Login_Time = 300 # time for LOGIN |
54 | -Release = "$Id$" | |
55 | -Revision = (r = /Revision: (\d+)/.match("$Revision$") ? r[1] : 0) | |
54 | +Revision = "20131104" | |
56 | 55 | |
57 | 56 | RELOAD_FILES = ["shogi_server/league/floodgate.rb", |
58 | 57 | "shogi_server/league/persistent.rb", |
@@ -43,17 +43,42 @@ EOF | ||
43 | 43 | |
44 | 44 | # Split a moves line into an array of a move string. |
45 | 45 | # If it fails to parse the moves, it raises WrongMoves. |
46 | - # @param moves a moves line. Ex. "+776FU-3334Fu" | |
47 | - # @return an array of a move string. Ex. ["+7776FU", "-3334FU"] | |
46 | + # @param moves a moves line. Ex. "+776FU-3334FU" or | |
47 | + # moves with times. Ex "+776FU,T2-3334FU,T5" | |
48 | + # @return an array of a move string. Ex. ["+7776FU", "-3334FU"] or | |
49 | + # an array of arrays. Ex. [["+7776FU","T2"], ["-3334FU", "T5"]] | |
48 | 50 | # |
49 | 51 | def Board.split_moves(moves) |
50 | 52 | ret = [] |
51 | 53 | |
52 | - rs = moves.gsub %r{[\+\-]\d{4}\w{2}} do |s| | |
53 | - ret << s | |
54 | - "" | |
55 | - end | |
56 | - raise WrongMoves, rs unless rs.empty? | |
54 | + i=0 | |
55 | + tmp = "" | |
56 | + while i<moves.size | |
57 | + if moves[i,1] == "+" || | |
58 | + moves[i,1] == "-" || | |
59 | + i == moves.size - 1 | |
60 | + if i == moves.size - 1 | |
61 | + tmp << moves[i,1] | |
62 | + end | |
63 | + unless tmp.empty? | |
64 | + a = tmp.split(",") | |
65 | + if a[0].size != 7 | |
66 | + raise WrongMoves, a[0] | |
67 | + end | |
68 | + if a.size == 1 # "+7776FU" | |
69 | + ret << a[0] | |
70 | + else # "+7776FU,T2" | |
71 | + unless /^T\d+/ =~ a[1] | |
72 | + raise WrongMoves, a[1] | |
73 | + end | |
74 | + ret << a | |
75 | + end | |
76 | + tmp = "" | |
77 | + end | |
78 | + end | |
79 | + tmp << moves[i,1] | |
80 | + i += 1 | |
81 | + end | |
57 | 82 | |
58 | 83 | return ret |
59 | 84 | end |
@@ -242,14 +267,21 @@ EOF | ||
242 | 267 | |
243 | 268 | # Set up a board starting with a position after the moves. |
244 | 269 | # Failing to parse the moves raises an ArgumentError. |
245 | - # @param moves an array of moves. ex. ["+7776FU", "-3334FU"] | |
270 | + # @param moves an array of moves. ex. ["+7776FU", "-3334FU"] or | |
271 | + # an array of arrays. ex. [["+7776FU","T2"], ["-3334FU","T5"]] | |
246 | 272 | # |
247 | 273 | def set_from_moves(moves) |
248 | 274 | initial() |
249 | 275 | return :normal if moves.empty? |
250 | 276 | rt = nil |
251 | 277 | moves.each do |move| |
252 | - rt = handle_one_move(move, @teban) | |
278 | + rt = nil | |
279 | + case move | |
280 | + when Array | |
281 | + rt = handle_one_move(move[0], @teban) | |
282 | + when String | |
283 | + rt = handle_one_move(move, @teban) | |
284 | + end | |
253 | 285 | raise ArgumentError, "bad moves: #{moves}" unless rt == :normal |
254 | 286 | end |
255 | 287 | @initial_moves = moves.dup |
@@ -21,7 +21,7 @@ module ShogiServer | ||
21 | 21 | end |
22 | 22 | |
23 | 23 | def ==(rhs) |
24 | - return (@game_name == rhs.game_name && | |
24 | + return (@game_name == rhs.game_name && | |
25 | 25 | @moves == rhs.moves && |
26 | 26 | @owner == rhs.owner && |
27 | 27 | @count == rhs.count) |
@@ -69,6 +69,9 @@ module ShogiServer | ||
69 | 69 | my_sente_str = $3 |
70 | 70 | cmd = GameChallengeCommand.new(str, player, |
71 | 71 | command_name, game_name, my_sente_str) |
72 | + when /^%%(GAME|CHALLENGE)\s+(\S+)/ | |
73 | + msg = "A turn identifier is required" | |
74 | + cmd = ErrorCommand.new(str, player, msg) | |
72 | 75 | when /^%%CHAT\s+(.+)/ |
73 | 76 | message = $1 |
74 | 77 | cmd = ChatCommand.new(str, player, message, $league.players) |
@@ -94,6 +97,19 @@ module ShogiServer | ||
94 | 97 | when /^%%GETBUOYCOUNT\s+(\S+)/ |
95 | 98 | game_name = $1 |
96 | 99 | cmd = GetBuoyCountCommand.new(str, player, game_name) |
100 | + when /^%%FORK\s+(\S+)\s+(\S+)(.*)/ | |
101 | + source_game = $1 | |
102 | + new_buoy_game = $2 | |
103 | + nth_move = nil | |
104 | + if $3 && /^\s+(\d+)/ =~ $3 | |
105 | + nth_move = $3.to_i | |
106 | + end | |
107 | + cmd = ForkCommand.new(str, player, source_game, new_buoy_game, nth_move) | |
108 | + when /^%%FORK\s+(\S+)$/ | |
109 | + source_game = $1 | |
110 | + new_buoy_game = nil | |
111 | + nth_move = nil | |
112 | + cmd = ForkCommand.new(str, player, source_game, new_buoy_game, nth_move) | |
97 | 113 | when /^\s*$/ |
98 | 114 | cmd = SpaceCommand.new(str, player) |
99 | 115 | when /^%%%[^%]/ |
@@ -481,7 +497,16 @@ module ShogiServer | ||
481 | 497 | |
482 | 498 | def call |
483 | 499 | if (! Login::good_game_name?(@game_name)) |
484 | - @player.write_safe(sprintf("##[ERROR] bad game name\n")) | |
500 | + @player.write_safe(sprintf("##[ERROR] bad game name: %s.\n", @game_name)) | |
501 | + if (/^(.+)-\d+-\d+$/ =~ @game_name) | |
502 | + if Login::good_identifier?($1) | |
503 | + # do nothing | |
504 | + else | |
505 | + @player.write_safe(sprintf("##[ERROR] invalid identifiers are found or too many characters are used.\n")) | |
506 | + end | |
507 | + else | |
508 | + @player.write_safe(sprintf("##[ERROR] game name should consist of three parts like game-1500-60.\n")) | |
509 | + end | |
485 | 510 | return :continue |
486 | 511 | elsif ((@player.status == "connected") || (@player.status == "game_waiting")) |
487 | 512 | ## continue |
@@ -666,9 +691,9 @@ module ShogiServer | ||
666 | 691 | # Command for an error |
667 | 692 | # |
668 | 693 | class ErrorCommand < Command |
669 | - def initialize(str, player) | |
670 | - super | |
671 | - @msg = nil | |
694 | + def initialize(str, player, msg=nil) | |
695 | + super(str, player) | |
696 | + @msg = msg || "unknown command" | |
672 | 697 | end |
673 | 698 | attr_reader :msg |
674 | 699 |
@@ -676,7 +701,7 @@ module ShogiServer | ||
676 | 701 | cmd = @str.chomp |
677 | 702 | # Aim to hide a possible password |
678 | 703 | cmd.gsub!(/LOGIN\s*(\w+)\s+.*/i, 'LOGIN \1...') |
679 | - @msg = "##[ERROR] unknown command %s\n" % [cmd] | |
704 | + @msg = "##[ERROR] %s: %s\n" % [@msg, cmd] | |
680 | 705 | @player.write_safe(@msg) |
681 | 706 | log_error(@msg) |
682 | 707 | return :continue |
@@ -810,4 +835,69 @@ module ShogiServer | ||
810 | 835 | end |
811 | 836 | end |
812 | 837 | |
838 | + # %%FORK <source_game> <new_buoy_game> [<nth-move>] | |
839 | + # Fork a new game from the posistion where the n-th (starting from 1) move | |
840 | + # of a source game is played. The new game should be a valid buoy game | |
841 | + # name. The default value of n is the position where the previous position | |
842 | + # of the last one. | |
843 | + # | |
844 | + class ForkCommand < Command | |
845 | + def initialize(str, player, source_game, new_buoy_game, nth_move) | |
846 | + super(str, player) | |
847 | + @source_game = source_game | |
848 | + @new_buoy_game = new_buoy_game | |
849 | + @nth_move = nth_move # may be nil | |
850 | + end | |
851 | + attr_reader :new_buoy_game | |
852 | + | |
853 | + def decide_new_buoy_game_name | |
854 | + name = nil | |
855 | + total_time = nil | |
856 | + byo_time = nil | |
857 | + | |
858 | + if @source_game.split("+").size >= 2 && | |
859 | + /^([^-]+)-(\d+)-(\d+)/ =~ @source_game.split("+")[1] | |
860 | + name = $1 | |
861 | + total_time = $2 | |
862 | + byo_time = $3 | |
863 | + end | |
864 | + if name == nil || total_time == nil || byo_time == nil | |
865 | + @player.write_safe(sprintf("##[ERROR] wrong source game name to make a new buoy game name: %s\n", @source_game)) | |
866 | + log_error "Received a wrong source game name to make a new buoy game name: %s from %s." % [@source_game, @player.name] | |
867 | + return :continue | |
868 | + end | |
869 | + @new_buoy_game = "buoy_%s_%d-%s-%s" % [name, @nth_move, total_time, byo_time] | |
870 | + @player.write_safe(sprintf("##[FORK]: new buoy game name: %s\n", @new_buoy_game)) | |
871 | + @player.write_safe("##[FORK] +OK\n") | |
872 | + end | |
873 | + | |
874 | + def call | |
875 | + game = $league.games[@source_game] | |
876 | + unless game | |
877 | + @player.write_safe(sprintf("##[ERROR] wrong source game name: %s\n", @source_game)) | |
878 | + log_error "Received a wrong source game name: %s from %s." % [@source_game, @player.name] | |
879 | + return :continue | |
880 | + end | |
881 | + | |
882 | + moves = game.read_moves # [["+7776FU","T2"],["-3334FU","T5"]] | |
883 | + @nth_move = moves.size - 1 unless @nth_move | |
884 | + if @nth_move > moves.size or @nth_move < 1 | |
885 | + @player.write_safe(sprintf("##[ERROR] number of moves to fork is out of range: %s.\n", moves.size)) | |
886 | + log_error "Number of moves to fork is out of range: %s [%s]" % [@nth_move, @player.name] | |
887 | + return :continue | |
888 | + end | |
889 | + new_moves_str = "" | |
890 | + moves[0...@nth_move].each do |m| | |
891 | + new_moves_str << m.join(",") | |
892 | + end | |
893 | + | |
894 | + unless @new_buoy_game | |
895 | + decide_new_buoy_game_name | |
896 | + end | |
897 | + | |
898 | + buoy_cmd = SetBuoyCommand.new(@str, @player, @new_buoy_game, new_moves_str, 1) | |
899 | + return buoy_cmd.call | |
900 | + end | |
901 | + end | |
902 | + | |
813 | 903 | end # module ShogiServer |
@@ -19,6 +19,7 @@ | ||
19 | 19 | |
20 | 20 | require 'shogi_server/league/floodgate' |
21 | 21 | require 'shogi_server/game_result' |
22 | +require 'shogi_server/time_clock' | |
22 | 23 | require 'shogi_server/util' |
23 | 24 | |
24 | 25 | module ShogiServer # for a namespace |
@@ -69,6 +70,8 @@ class Game | ||
69 | 70 | if (@game_name =~ /-(\d+)-(\d+)$/) |
70 | 71 | @total_time = $1.to_i |
71 | 72 | @byoyomi = $2.to_i |
73 | + | |
74 | + @time_clock = TimeClock::factory(Least_Time_Per_Move, @game_name) | |
72 | 75 | end |
73 | 76 | |
74 | 77 | if (player0.sente) |
@@ -87,7 +90,16 @@ class Game | ||
87 | 90 | @sente.game = self |
88 | 91 | @gote.game = self |
89 | 92 | |
90 | - @last_move = @board.initial_moves.empty? ? "" : "%s,T1" % [@board.initial_moves.last] | |
93 | + @last_move = "" | |
94 | + unless @board.initial_moves.empty? | |
95 | + last_move = @board.initial_moves.last | |
96 | + case last_move | |
97 | + when Array | |
98 | + @last_move = last_move.join(",") | |
99 | + when String | |
100 | + @last_move = "%s,T1" % [last_move] | |
101 | + end | |
102 | + end | |
91 | 103 | @current_turn = @board.initial_moves.size |
92 | 104 | |
93 | 105 | @sente.status = "agree_waiting" |
@@ -110,6 +122,7 @@ class Game | ||
110 | 122 | $league.games[@game_id] = self |
111 | 123 | |
112 | 124 | log_message(sprintf("game created %s", @game_id)) |
125 | + log_message(" " + @time_clock.to_s) | |
113 | 126 | |
114 | 127 | @start_time = nil |
115 | 128 | @fh = open(@logfile, "w") |
@@ -118,7 +131,7 @@ class Game | ||
118 | 131 | |
119 | 132 | propose |
120 | 133 | end |
121 | - attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors | |
134 | + attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors, :time_clock | |
122 | 135 | attr_accessor :last_move, :current_turn |
123 | 136 | attr_reader :result, :prepared_time |
124 | 137 |
@@ -218,22 +231,16 @@ class Game | ||
218 | 231 | return nil |
219 | 232 | end |
220 | 233 | |
221 | - finish_flag = true | |
222 | 234 | @end_time = end_time |
223 | - t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max | |
224 | - | |
235 | + finish_flag = true | |
225 | 236 | move_status = nil |
226 | - if ((@current_player.mytime - t <= -@byoyomi) && | |
227 | - ((@total_time > 0) || (@byoyomi > 0))) | |
237 | + | |
238 | + if (@time_clock.timeout?(@current_player, @start_time, @end_time)) | |
228 | 239 | status = :timeout |
229 | 240 | elsif (str == :timeout) |
230 | 241 | return false # time isn't expired. players aren't swapped. continue game |
231 | 242 | else |
232 | - @current_player.mytime -= t | |
233 | - if (@current_player.mytime < 0) | |
234 | - @current_player.mytime = 0 | |
235 | - end | |
236 | - | |
243 | + t = @time_clock.process_time(@current_player, @start_time, @end_time) | |
237 | 244 | move_status = @board.handle_one_move(str, @sente == @current_player) |
238 | 245 | # log_debug("move_status: %s for %s's %s" % [move_status, @sente == @current_player ? "BLACK" : "WHITE", str]) |
239 | 246 |
@@ -332,8 +339,14 @@ class Game | ||
332 | 339 | unless @board.initial_moves.empty? |
333 | 340 | @fh.puts "'buoy game starting with %d moves" % [@board.initial_moves.size] |
334 | 341 | @board.initial_moves.each do |move| |
335 | - @fh.puts move | |
336 | - @fh.puts "T1" | |
342 | + case move | |
343 | + when Array | |
344 | + @fh.puts move[0] | |
345 | + @fh.puts move[1] | |
346 | + when String | |
347 | + @fh.puts move | |
348 | + @fh.puts "T1" | |
349 | + end | |
337 | 350 | end |
338 | 351 | end |
339 | 352 | end |
@@ -351,7 +364,7 @@ Name-:#{@gote.name} | ||
351 | 364 | Rematch_On_Draw:NO |
352 | 365 | To_Move:+ |
353 | 366 | BEGIN Time |
354 | -Time_Unit:1sec | |
367 | +Time_Unit:#{@time_clock.time_unit} | |
355 | 368 | Total_Time:#{@total_time} |
356 | 369 | Byoyomi:#{@byoyomi} |
357 | 370 | Least_Time_Per_Move:#{Least_Time_Per_Move} |
@@ -385,14 +398,21 @@ Your_Turn:#{sg_flag} | ||
385 | 398 | Rematch_On_Draw:NO |
386 | 399 | To_Move:#{@board.teban ? "+" : "-"} |
387 | 400 | BEGIN Time |
388 | -Time_Unit:1sec | |
401 | +Time_Unit:#{@time_clock.time_unit} | |
389 | 402 | Total_Time:#{@total_time} |
390 | 403 | Byoyomi:#{@byoyomi} |
391 | 404 | Least_Time_Per_Move:#{Least_Time_Per_Move} |
392 | 405 | END Time |
393 | 406 | BEGIN Position |
394 | 407 | #{@board.initial_string.chomp} |
395 | -#{@board.initial_moves.collect {|m| m + ",T1"}.join("\n")} | |
408 | +#{@board.initial_moves.collect do |m| | |
409 | + case m | |
410 | + when Array | |
411 | + m.join(",") | |
412 | + when String | |
413 | + m + ",T1" | |
414 | + end | |
415 | +end.join("\n")} | |
396 | 416 | END Position |
397 | 417 | END Game_Summary |
398 | 418 | EOM |
@@ -408,6 +428,21 @@ EOM | ||
408 | 428 | |
409 | 429 | return false |
410 | 430 | end |
431 | + | |
432 | + # Read the .csa file and returns an array of moves and times. | |
433 | + # ex. [["+7776FU","T2"], ["-3334FU","T5"]] | |
434 | + # | |
435 | + def read_moves | |
436 | + ret = [] | |
437 | + IO.foreach(@logfile) do |line| | |
438 | + if /^[\+\-]\d{4}[A-Z]{2}/ =~ line | |
439 | + ret << [line.chomp] | |
440 | + elsif /^T\d*/ =~ line | |
441 | + ret[-1] << line.chomp | |
442 | + end | |
443 | + end | |
444 | + return ret | |
445 | + end | |
411 | 446 | |
412 | 447 | private |
413 | 448 |
@@ -257,6 +257,18 @@ class League | ||
257 | 257 | return rc[:loser] == player_id |
258 | 258 | end |
259 | 259 | |
260 | + def last_opponent(player_id) | |
261 | + rc = last_valid_game(player_id) | |
262 | + return nil unless rc | |
263 | + if rc[:black] == player_id | |
264 | + return rc[:white] | |
265 | + elsif rc[:white] == player_id | |
266 | + return rc[:black] | |
267 | + else | |
268 | + return nil | |
269 | + end | |
270 | + end | |
271 | + | |
260 | 272 | def last_valid_game(player_id) |
261 | 273 | records = nil |
262 | 274 | @@mutex.synchronize do |
@@ -269,6 +281,28 @@ class League | ||
269 | 281 | end |
270 | 282 | return rc |
271 | 283 | end |
284 | + | |
285 | + def win_games(player_id) | |
286 | + records = nil | |
287 | + @@mutex.synchronize do | |
288 | + records = @records.reverse | |
289 | + end | |
290 | + rc = records.find_all do |rc| | |
291 | + rc[:winner] == player_id && rc[:loser] | |
292 | + end | |
293 | + return rc | |
294 | + end | |
295 | + | |
296 | + def loss_games(player_id) | |
297 | + records = nil | |
298 | + @@mutex.synchronize do | |
299 | + records = @records.reverse | |
300 | + end | |
301 | + rc = records.find_all do |rc| | |
302 | + rc[:winner] && rc[:loser] == player_id | |
303 | + end | |
304 | + return rc | |
305 | + end | |
272 | 306 | end # class History |
273 | 307 | |
274 | 308 |
@@ -25,7 +25,7 @@ module ShogiServer | ||
25 | 25 | |
26 | 26 | class << self |
27 | 27 | def default_factory |
28 | - return swiss_pairing | |
28 | + return least_diff_pairing | |
29 | 29 | end |
30 | 30 | |
31 | 31 | def sort_by_rate_with_randomness |
@@ -52,6 +52,14 @@ module ShogiServer | ||
52 | 52 | StartGameWithoutHumans.new] |
53 | 53 | end |
54 | 54 | |
55 | + def least_diff_pairing | |
56 | + return [LogPlayers.new, | |
57 | + ExcludeSacrificeGps500.new, | |
58 | + MakeEven.new, | |
59 | + LeastDiff.new, | |
60 | + StartGameWithoutHumans.new] | |
61 | + end | |
62 | + | |
55 | 63 | def match(players) |
56 | 64 | logics = default_factory |
57 | 65 | logics.inject(players) do |result, item| |
@@ -62,6 +70,10 @@ module ShogiServer | ||
62 | 70 | end # class << self |
63 | 71 | |
64 | 72 | |
73 | + # Make matches among players. | |
74 | + # @param players an array of players, which should be updated destructively | |
75 | + # to pass the new list to subsequent logics. | |
76 | + # | |
65 | 77 | def match(players) |
66 | 78 | # to be implemented |
67 | 79 | log_message("Floodgate: %s" % [self.class.to_s]) |
@@ -232,17 +244,18 @@ module ShogiServer | ||
232 | 244 | end |
233 | 245 | |
234 | 246 | class SortByRateWithRandomness < Pairing |
235 | - def initialize(rand1, rand2) | |
247 | + def initialize(rand1, rand2, desc=false) | |
236 | 248 | super() |
237 | 249 | @rand1, @rand2 = rand1, rand2 |
250 | + @desc = desc | |
238 | 251 | end |
239 | 252 | |
240 | - def match(players, desc=false) | |
253 | + def match(players) | |
241 | 254 | super(players) |
242 | 255 | cur_rate = Hash.new |
243 | 256 | players.each{|a| cur_rate[a] = a.rate ? a.rate + rand(@rand1) : rand(@rand2)} |
244 | 257 | players.sort!{|a,b| cur_rate[a] <=> cur_rate[b]} |
245 | - players.reverse! if desc | |
258 | + players.reverse! if @desc | |
246 | 259 | log_players(players) do |one| |
247 | 260 | "%s %d (+ randomness %d)" % [one.name, one.rate, cur_rate[one] - one.rate] |
248 | 261 | end |
@@ -267,12 +280,12 @@ module ShogiServer | ||
267 | 280 | rest = players - winners |
268 | 281 | |
269 | 282 | log_message("Floodgate: Ordering %d winners..." % [winners.size]) |
270 | - sbrwr_winners = SortByRateWithRandomness.new(800, 2500) | |
271 | - sbrwr_winners.match(winners, true) | |
283 | + sbrwr_winners = SortByRateWithRandomness.new(800, 2500, true) | |
284 | + sbrwr_winners.match(winners) | |
272 | 285 | |
273 | 286 | log_message("Floodgate: Ordering the rest (%d)..." % [rest.size]) |
274 | - sbrwr_losers = SortByRateWithRandomness.new(200, 400) | |
275 | - sbrwr_losers.match(rest, true) | |
287 | + sbrwr_losers = SortByRateWithRandomness.new(200, 400, true) | |
288 | + sbrwr_losers.match(rest) | |
276 | 289 | |
277 | 290 | players.clear |
278 | 291 | [winners, rest].each do |group| |
@@ -364,4 +377,126 @@ module ShogiServer | ||
364 | 377 | end |
365 | 378 | end |
366 | 379 | |
380 | + # This pairing algorithm aims to minimize the total differences of | |
381 | + # matching players' rates. It also includes penalyties when a match is | |
382 | + # same as the previous one or a match is between human players. | |
383 | + # It is based on a discussion with Yamashita-san on | |
384 | + # http://www.sgtpepper.net/kaneko/diary/20120511.html. | |
385 | + # | |
386 | + class LeastDiff < Pairing | |
387 | + def random_match(players) | |
388 | + players.shuffle | |
389 | + end | |
390 | + | |
391 | + # Returns a player's rate value. | |
392 | + # 1. If it has a valid rate, return the rate. | |
393 | + # 2. If it has no valid rate, return average of the following values: | |
394 | + # a. For games it won, the opponent's rate + 100 | |
395 | + # b. For games it lost, the opponent's rate - 100 | |
396 | + # (if the opponent has no valid rate, count out the game) | |
397 | + # (if there are not such games, return 2150 (default value) | |
398 | + # | |
399 | + def get_player_rate(player, history) | |
400 | + return player.rate if player.rate != 0 | |
401 | + return 2150 unless history | |
402 | + | |
403 | + count = 0 | |
404 | + sum = 0 | |
405 | + | |
406 | + history.win_games(player.player_id).each do |g| | |
407 | + next unless g[:loser] | |
408 | + name = g[:loser].split("+")[0] | |
409 | + p = $league.find(name) | |
410 | + if p && p.rate != 0 | |
411 | + count += 1 | |
412 | + sum += p.rate + 100 | |
413 | + end | |
414 | + end | |
415 | + history.loss_games(player.player_id).each do |g| | |
416 | + next unless g[:winner] | |
417 | + name = g[:winner].split("+")[0] | |
418 | + p = $league.find(name) | |
419 | + if p && p.rate != 0 | |
420 | + count += 1 | |
421 | + sum += p.rate - 100 | |
422 | + end | |
423 | + end | |
424 | + | |
425 | + estimate = (count == 0 ? 2150 : sum/count) | |
426 | + log_message("Floodgate: Estimated rate of %s is %d" % [player.name, estimate]) | |
427 | + return estimate | |
428 | + end | |
429 | + | |
430 | + def calculate_diff_with_penalty(players, history) | |
431 | + pairs = [] | |
432 | + players.each_slice(2) do |pair| | |
433 | + if pair.size == 2 | |
434 | + pairs << pair | |
435 | + end | |
436 | + end | |
437 | + | |
438 | + ret = 0 | |
439 | + | |
440 | + # 1. Diff of players rate | |
441 | + pairs.each do |p1,p2| | |
442 | + ret += (get_player_rate(p1,history) - get_player_rate(p2,history)).abs | |
443 | + end | |
444 | + | |
445 | + # 2. Penalties | |
446 | + pairs.each do |p1,p2| | |
447 | + # 2.1. same match | |
448 | + if (history && | |
449 | + (history.last_opponent(p1.player_id) == p2.player_id || | |
450 | + history.last_opponent(p2.player_id) == p1.player_id)) | |
451 | + ret += 400 | |
452 | + end | |
453 | + | |
454 | + # 2.2 Human vs Human | |
455 | + if p1.is_human? && p2.is_human? | |
456 | + ret += 800 | |
457 | + end | |
458 | + end | |
459 | + | |
460 | + ret | |
461 | + end | |
462 | + | |
463 | + def match(players) | |
464 | + super | |
465 | + if players.size < 3 | |
466 | + log_message("Floodgate: players are small enough to skip LeastDiff pairing: %d" % [players.size]) | |
467 | + return players | |
468 | + end | |
469 | + | |
470 | + # 10 trials | |
471 | + matches = [] | |
472 | + scores = [] | |
473 | + path = ShogiServer::League::Floodgate.history_file_path(players.first.game_name) | |
474 | + history = ShogiServer::League::Floodgate::History.factory(path) | |
475 | + 10.times do | |
476 | + m = random_match(players) | |
477 | + matches << m | |
478 | + scores << calculate_diff_with_penalty(m, history) | |
479 | + end | |
480 | + | |
481 | + # Debug | |
482 | + #scores.each_with_index do |s,i| | |
483 | + # puts | |
484 | + # print s, ": ", matches[i].map{|p| p.name}.join(", "), "\n" | |
485 | + #end | |
486 | + | |
487 | + # Select a match of the least score | |
488 | + min_index = 0 | |
489 | + min_score = scores.first | |
490 | + scores.each_with_index do |s,i| | |
491 | + if s < min_score | |
492 | + min_index = i | |
493 | + min_score = s | |
494 | + end | |
495 | + end | |
496 | + log_message("Floodgate: the least score %d (%d per player) [%s]" % [min_score, min_score/players.size, scores.join(" ")]) | |
497 | + | |
498 | + players.replace(matches[min_index]) | |
499 | + end | |
500 | + end | |
501 | + | |
367 | 502 | end # ShogiServer |
@@ -0,0 +1,139 @@ | ||
1 | +## $Id$ | |
2 | + | |
3 | +## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch) | |
4 | +## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org) | |
5 | +## | |
6 | +## This program is free software; you can redistribute it and/or modify | |
7 | +## it under the terms of the GNU General Public License as published by | |
8 | +## the Free Software Foundation; either version 2 of the License, or | |
9 | +## (at your option) any later version. | |
10 | +## | |
11 | +## This program is distributed in the hope that it will be useful, | |
12 | +## but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | +## GNU General Public License for more details. | |
15 | +## | |
16 | +## You should have received a copy of the GNU General Public License | |
17 | +## along with this program; if not, write to the Free Software | |
18 | +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
19 | + | |
20 | +module ShogiServer # for a namespace | |
21 | + | |
22 | +# Abstract class to caclulate thinking time. | |
23 | +# | |
24 | +class TimeClock | |
25 | + | |
26 | + def TimeClock.factory(least_time_per_move, game_name) | |
27 | + total_time_str = nil | |
28 | + byoyomi_str = nil | |
29 | + if (game_name =~ /-(\d+)-(\d+)$/) | |
30 | + total_time_str = $1 | |
31 | + byoyomi_str = $2 | |
32 | + end | |
33 | + total_time = total_time_str.to_i | |
34 | + byoyomi = byoyomi_str.to_i | |
35 | + | |
36 | + if (byoyomi_str == "060") | |
37 | + @time_clock = StopWatchClock.new(least_time_per_move, total_time, byoyomi) | |
38 | + else | |
39 | + @time_clock = ChessClock.new(least_time_per_move, total_time, byoyomi) | |
40 | + end | |
41 | + end | |
42 | + | |
43 | + def initialize(least_time_per_move, total_time, byoyomi) | |
44 | + @least_time_per_move = least_time_per_move | |
45 | + @total_time = total_time | |
46 | + @byoyomi = byoyomi | |
47 | + end | |
48 | + | |
49 | + # Returns thinking time duration | |
50 | + # | |
51 | + def time_duration(start_time, end_time) | |
52 | + # implement this | |
53 | + return 9999999 | |
54 | + end | |
55 | + | |
56 | + # Returns what "Time_Unit:" in CSA protocol should provide. | |
57 | + # | |
58 | + def time_unit | |
59 | + return "1sec" | |
60 | + end | |
61 | + | |
62 | + # If thinking time runs out, returns true; false otherwise. | |
63 | + # | |
64 | + def timeout?(player, start_time, end_time) | |
65 | + # implement this | |
66 | + return true | |
67 | + end | |
68 | + | |
69 | + # Updates a player's remaining time and returns thinking time. | |
70 | + # | |
71 | + def process_time(player, start_time, end_time) | |
72 | + t = time_duration(start_time, end_time) | |
73 | + | |
74 | + player.mytime -= t | |
75 | + if (player.mytime < 0) | |
76 | + player.mytime = 0 | |
77 | + end | |
78 | + | |
79 | + return t | |
80 | + end | |
81 | +end | |
82 | + | |
83 | +# Calculates thinking time with chess clock. | |
84 | +# | |
85 | +class ChessClock < TimeClock | |
86 | + def initialize(least_time_per_move, total_time, byoyomi) | |
87 | + super | |
88 | + end | |
89 | + | |
90 | + def time_duration(start_time, end_time) | |
91 | + return [(end_time - start_time).floor, @least_time_per_move].max | |
92 | + end | |
93 | + | |
94 | + def timeout?(player, start_time, end_time) | |
95 | + t = time_duration(start_time, end_time) | |
96 | + | |
97 | + if ((player.mytime - t <= -@byoyomi) && | |
98 | + ((@total_time > 0) || (@byoyomi > 0))) | |
99 | + return true | |
100 | + else | |
101 | + return false | |
102 | + end | |
103 | + end | |
104 | + | |
105 | + def to_s | |
106 | + return "ChessClock: LeastTimePerMove %d; TotalTime %d; Byoyomi %d" % [@least_time_per_move, @total_time, @byoyomi] | |
107 | + end | |
108 | +end | |
109 | + | |
110 | +class StopWatchClock < TimeClock | |
111 | + def initialize(least_time_per_move, total_time, byoyomi) | |
112 | + super | |
113 | + end | |
114 | + | |
115 | + def time_unit | |
116 | + return "1min" | |
117 | + end | |
118 | + | |
119 | + def time_duration(start_time, end_time) | |
120 | + t = [(end_time - start_time).floor, @least_time_per_move].max | |
121 | + return (t / @byoyomi) * @byoyomi | |
122 | + end | |
123 | + | |
124 | + def timeout?(player, start_time, end_time) | |
125 | + t = time_duration(start_time, end_time) | |
126 | + | |
127 | + if (player.mytime <= t) | |
128 | + return true | |
129 | + else | |
130 | + return false | |
131 | + end | |
132 | + end | |
133 | + | |
134 | + def to_s | |
135 | + return "StopWatchClock: LeastTimePerMove %d; TotalTime %d; Byoyomi %d" % [@least_time_per_move, @total_time, @byoyomi] | |
136 | + end | |
137 | +end | |
138 | + | |
139 | +end |
@@ -10,6 +10,7 @@ require 'TC_floodgate' | ||
10 | 10 | require 'TC_floodgate_history' |
11 | 11 | require 'TC_floodgate_next_time_generator' |
12 | 12 | require 'TC_floodgate_thread.rb' |
13 | +require 'TC_fork' | |
13 | 14 | require 'TC_functional' |
14 | 15 | require 'TC_game' |
15 | 16 | require 'TC_game_result' |
@@ -24,6 +25,7 @@ require 'TC_oute_sennichite' | ||
24 | 25 | require 'TC_pairing' |
25 | 26 | require 'TC_player' |
26 | 27 | require 'TC_rating' |
28 | +require 'TC_time_clock' | |
27 | 29 | require 'TC_uchifuzume' |
28 | 30 | require 'TC_usi' |
29 | 31 | require 'TC_util' |
@@ -228,6 +228,16 @@ class TestFactoryMethod < Test::Unit::TestCase | ||
228 | 228 | assert_instance_of(ShogiServer::GetBuoyCountCommand, cmd) |
229 | 229 | end |
230 | 230 | |
231 | + def test_fork_command | |
232 | + cmd = ShogiServer::Command.factory("%%FORK server-denou-14400-60+p1+p2+20130223185013 buoy_denou-14400-60", @p) | |
233 | + assert_instance_of(ShogiServer::ForkCommand, cmd) | |
234 | + end | |
235 | + | |
236 | + def test_fork_command2 | |
237 | + cmd = ShogiServer::Command.factory("%%FORK server-denou-14400-60+p1+p2+20130223185013", @p) | |
238 | + assert_instance_of(ShogiServer::ForkCommand, cmd) | |
239 | + end | |
240 | + | |
231 | 241 | def test_void_command |
232 | 242 | cmd = ShogiServer::Command.factory("%%%HOGE", @p) |
233 | 243 | assert_instance_of(ShogiServer::VoidCommand, cmd) |
@@ -237,29 +247,29 @@ class TestFactoryMethod < Test::Unit::TestCase | ||
237 | 247 | cmd = ShogiServer::Command.factory("should_be_error", @p) |
238 | 248 | assert_instance_of(ShogiServer::ErrorCommand, cmd) |
239 | 249 | cmd.call |
240 | - assert_match /unknown command should_be_error/, cmd.msg | |
250 | + assert_match /unknown command: should_be_error/, cmd.msg | |
241 | 251 | end |
242 | 252 | |
243 | 253 | def test_error_login |
244 | 254 | cmd = ShogiServer::Command.factory("LOGIN hoge foo", @p) |
245 | 255 | assert_instance_of(ShogiServer::ErrorCommand, cmd) |
246 | 256 | cmd.call |
247 | - assert_no_match /unknown command LOGIN hoge foo/, cmd.msg | |
257 | + assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg | |
248 | 258 | |
249 | 259 | cmd = ShogiServer::Command.factory("LOGin hoge foo", @p) |
250 | 260 | assert_instance_of(ShogiServer::ErrorCommand, cmd) |
251 | 261 | cmd.call |
252 | - assert_no_match /unknown command LOGIN hoge foo/, cmd.msg | |
262 | + assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg | |
253 | 263 | |
254 | 264 | cmd = ShogiServer::Command.factory("LOGIN hoge foo", @p) |
255 | 265 | assert_instance_of(ShogiServer::ErrorCommand, cmd) |
256 | 266 | cmd.call |
257 | - assert_no_match /unknown command LOGIN hoge foo/, cmd.msg | |
267 | + assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg | |
258 | 268 | |
259 | 269 | cmd = ShogiServer::Command.factory("LOGINhoge foo", @p) |
260 | 270 | assert_instance_of(ShogiServer::ErrorCommand, cmd) |
261 | 271 | cmd.call |
262 | - assert_no_match /unknown command LOGIN hoge foo/, cmd.msg | |
272 | + assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg | |
263 | 273 | end |
264 | 274 | end |
265 | 275 |
@@ -939,6 +949,28 @@ end | ||
939 | 949 | |
940 | 950 | # |
941 | 951 | # |
952 | +class TestForkCommand < Test::Unit::TestCase | |
953 | + def setup | |
954 | + @player = MockPlayer.new | |
955 | + end | |
956 | + | |
957 | + def test_new_buoy_game_name | |
958 | + src = "%%FORK server+denou-14400-60+p1+p2+20130223185013" | |
959 | + c = ShogiServer::ForkCommand.new src, @player, "server+denou-14400-60+p1+p2+20130223185013", nil, 13 | |
960 | + c.decide_new_buoy_game_name | |
961 | + assert_equal "buoy_denou_13-14400-60", c.new_buoy_game | |
962 | + end | |
963 | + | |
964 | + def test_new_buoy_game_name2 | |
965 | + src = "%%FORK server+denou-14400-060+p1+p2+20130223185013" | |
966 | + c = ShogiServer::ForkCommand.new src, @player, "server+denou-14400-060+p1+p2+20130223185013", nil, 13 | |
967 | + c.decide_new_buoy_game_name | |
968 | + assert_equal "buoy_denou_13-14400-060", c.new_buoy_game | |
969 | + end | |
970 | +end | |
971 | + | |
972 | +# | |
973 | +# | |
942 | 974 | class TestGetBuoyCountCommand < BaseTestBuoyCommand |
943 | 975 | def test_call |
944 | 976 | buoy_game = ShogiServer::BuoyGame.new("buoy_testdeletebuoy-1500-0", "+7776FU", @p.name, 1) |
@@ -1051,4 +1083,3 @@ class TestMonitorHandler2 < Test::Unit::TestCase | ||
1051 | 1083 | @player.out.join) |
1052 | 1084 | end |
1053 | 1085 | end |
1054 | - |
@@ -395,6 +395,22 @@ class TestFloodgateHistory < Test::Unit::TestCase | ||
395 | 395 | assert !@history.last_win?("foo") |
396 | 396 | assert !@history.last_lose?("hoge") |
397 | 397 | assert @history.last_lose?("foo") |
398 | + | |
399 | + assert_equal("foo", @history.last_opponent("hoge")) | |
400 | + assert_equal("hoge", @history.last_opponent("foo")) | |
401 | + | |
402 | + games = @history.win_games("hoge") | |
403 | + assert_equal(1, games.size ) | |
404 | + assert_equal("wdoor+floodgate-900-0-hoge-foo-2", games[0][:game_id]) | |
405 | + games = @history.win_games("foo") | |
406 | + assert_equal(1, games.size ) | |
407 | + assert_equal("wdoor+floodgate-900-0-hoge-foo-1", games[0][:game_id]) | |
408 | + games = @history.loss_games("hoge") | |
409 | + assert_equal(1, games.size ) | |
410 | + assert_equal("wdoor+floodgate-900-0-hoge-foo-1", games[0][:game_id]) | |
411 | + games = @history.loss_games("foo") | |
412 | + assert_equal(1, games.size ) | |
413 | + assert_equal("wdoor+floodgate-900-0-hoge-foo-2", games[0][:game_id]) | |
398 | 414 | end |
399 | 415 | end |
400 | 416 |
@@ -0,0 +1,131 @@ | ||
1 | +$:.unshift File.join(File.dirname(__FILE__), "..") | |
2 | +$topdir = File.expand_path File.dirname(__FILE__) | |
3 | +require "baseclient" | |
4 | +require "shogi_server/buoy.rb" | |
5 | + | |
6 | +class TestFork < BaseClient | |
7 | + def parse_game_name(player) | |
8 | + player.puts "%%LIST" | |
9 | + sleep 1 | |
10 | + if /##\[LIST\] (.*)/ =~ player.message | |
11 | + return $1 | |
12 | + end | |
13 | + end | |
14 | + | |
15 | + def test_wrong_game | |
16 | + @admin = SocketPlayer.new "dummy", "admin", false | |
17 | + @admin.connect | |
18 | + @admin.reader | |
19 | + @admin.login | |
20 | + | |
21 | + result, result2 = handshake do | |
22 | + @admin.puts "%%FORK wronggame-900-0 buoy_WrongGame-900-0" | |
23 | + sleep 1 | |
24 | + end | |
25 | + | |
26 | + assert /##\[ERROR\] wrong source game name/ =~ @admin.message | |
27 | + @admin.logout | |
28 | + end | |
29 | + | |
30 | + def test_too_short_fork | |
31 | + @admin = SocketPlayer.new "dummy", "admin", false | |
32 | + @admin.connect | |
33 | + @admin.reader | |
34 | + @admin.login | |
35 | + | |
36 | + result, result2 = handshake do | |
37 | + source_game = parse_game_name(@admin) | |
38 | + @admin.puts "%%FORK #{source_game} buoy_TooShortFork-900-0 0" | |
39 | + sleep 1 | |
40 | + end | |
41 | + | |
42 | + assert /##\[ERROR\] number of moves to fork is out of range/ =~ @admin.message | |
43 | + @admin.logout | |
44 | + end | |
45 | + | |
46 | + def test_fork | |
47 | + buoy = ShogiServer::Buoy.new | |
48 | + | |
49 | + @admin = SocketPlayer.new "dummy", "admin", "*" | |
50 | + @admin.connect | |
51 | + @admin.reader | |
52 | + @admin.login | |
53 | + assert buoy.is_new_game?("buoy_Fork-1500-0") | |
54 | + | |
55 | + result, result2 = handshake do | |
56 | + source_game = parse_game_name(@admin) | |
57 | + @admin.puts "%%FORK #{source_game} buoy_Fork-1500-0" | |
58 | + sleep 1 | |
59 | + end | |
60 | + | |
61 | + assert buoy.is_new_game?("buoy_Fork-1500-0") | |
62 | + @p1 = SocketPlayer.new "buoy_Fork", "p1", true | |
63 | + @p2 = SocketPlayer.new "buoy_Fork", "p2", false | |
64 | + @p1.connect | |
65 | + @p2.connect | |
66 | + @p1.reader | |
67 | + @p2.reader | |
68 | + @p1.login | |
69 | + @p2.login | |
70 | + sleep 1 | |
71 | + @p1.game | |
72 | + @p2.game | |
73 | + sleep 1 | |
74 | + @p1.agree | |
75 | + @p2.agree | |
76 | + sleep 1 | |
77 | + assert /^Total_Time:1500/ =~ @p1.message | |
78 | + assert /^Total_Time:1500/ =~ @p2.message | |
79 | + @p2.move("-3334FU") | |
80 | + sleep 1 | |
81 | + @p1.toryo | |
82 | + sleep 1 | |
83 | + @p2.logout | |
84 | + @p1.logout | |
85 | + | |
86 | + @admin.logout | |
87 | + end | |
88 | + | |
89 | + def test_fork2 | |
90 | + buoy = ShogiServer::Buoy.new | |
91 | + | |
92 | + @admin = SocketPlayer.new "dummy", "admin", "*" | |
93 | + @admin.connect | |
94 | + @admin.reader | |
95 | + @admin.login | |
96 | + | |
97 | + result, result2 = handshake do | |
98 | + source_game = parse_game_name(@admin) | |
99 | + @admin.puts "%%FORK #{source_game}" # nil for new_buoy_game name | |
100 | + sleep 1 | |
101 | + assert /##\[FORK\]: new buoy game name: buoy_TestFork_1-1500-0/ =~ @admin.message | |
102 | + end | |
103 | + | |
104 | + assert buoy.is_new_game?("buoy_TestFork_1-1500-0") | |
105 | + @p1 = SocketPlayer.new "buoy_TestFork_1", "p1", true | |
106 | + @p2 = SocketPlayer.new "buoy_TestFork_1", "p2", false | |
107 | + @p1.connect | |
108 | + @p2.connect | |
109 | + @p1.reader | |
110 | + @p2.reader | |
111 | + @p1.login | |
112 | + @p2.login | |
113 | + sleep 1 | |
114 | + @p1.game | |
115 | + @p2.game | |
116 | + sleep 1 | |
117 | + @p1.agree | |
118 | + @p2.agree | |
119 | + sleep 1 | |
120 | + assert /^Total_Time:1500/ =~ @p1.message | |
121 | + assert /^Total_Time:1500/ =~ @p2.message | |
122 | + @p2.move("-3334FU") | |
123 | + sleep 1 | |
124 | + @p1.toryo | |
125 | + sleep 1 | |
126 | + @p2.logout | |
127 | + @p1.logout | |
128 | + | |
129 | + @admin.logout | |
130 | + end | |
131 | +end |
@@ -1,11 +1,11 @@ | ||
1 | 1 | $:.unshift File.join(File.dirname(__FILE__), "..") |
2 | 2 | require 'test/unit' |
3 | 3 | require 'shogi_server' |
4 | +require 'shogi_server/league.rb' | |
4 | 5 | require 'shogi_server/player' |
5 | 6 | require 'shogi_server/pairing' |
6 | 7 | require 'test/mock_log_message' |
7 | 8 | |
8 | - | |
9 | 9 | def same_pair?(a, b) |
10 | 10 | unless a.size == 2 && b.size == 2 |
11 | 11 | return false |
@@ -327,4 +327,247 @@ class TestStartGameWithoutHumans < Test::Unit::TestCase | ||
327 | 327 | end |
328 | 328 | end |
329 | 329 | |
330 | +class TestLeastDiff < Test::Unit::TestCase | |
331 | + | |
332 | + class MockLeague | |
333 | + def initialize | |
334 | + @players = [] | |
335 | + end | |
336 | + | |
337 | + def add(player) | |
338 | + @players << player | |
339 | + end | |
340 | + | |
341 | + def find(name) | |
342 | + @players.find do |p| | |
343 | + p.name == name | |
344 | + end | |
345 | + end | |
346 | + end | |
347 | + | |
348 | + def setup | |
349 | + $league = MockLeague.new | |
350 | + | |
351 | + @pairing= ShogiServer::LeastDiff.new | |
352 | + $paired = [] | |
353 | + $called = 0 | |
354 | + def @pairing.start_game(p1,p2) | |
355 | + $called += 1 | |
356 | + $paired << [p1,p2] | |
357 | + end | |
358 | + | |
359 | + @file = Pathname.new(File.join(File.dirname(__FILE__), "floodgate_history.yaml")) | |
360 | + @history = ShogiServer::League::Floodgate::History.new @file | |
361 | + | |
362 | + @a = ShogiServer::BasicPlayer.new | |
363 | + @a.player_id = "a" | |
364 | + @a.name = "a" | |
365 | + @a.win = 1 | |
366 | + @a.loss = 2 | |
367 | + @a.rate = 500 | |
368 | + @b = ShogiServer::BasicPlayer.new | |
369 | + @b.player_id = "b" | |
370 | + @b.name = "b" | |
371 | + @b.win = 10 | |
372 | + @b.loss = 20 | |
373 | + @b.rate = 800 | |
374 | + @c = ShogiServer::BasicPlayer.new | |
375 | + @c.player_id = "c" | |
376 | + @c.name = "c" | |
377 | + @c.win = 100 | |
378 | + @c.loss = 200 | |
379 | + @c.rate = 1000 | |
380 | + @d = ShogiServer::BasicPlayer.new | |
381 | + @d.player_id = "d" | |
382 | + @d.name = "d" | |
383 | + @d.win = 1000 | |
384 | + @d.loss = 2000 | |
385 | + @d.rate = 1500 | |
386 | + @e = ShogiServer::BasicPlayer.new | |
387 | + @e.player_id = "e" | |
388 | + @e.name = "e" | |
389 | + @e.win = 3000 | |
390 | + @e.loss = 3000 | |
391 | + @e.rate = 2000 | |
392 | + @f = ShogiServer::BasicPlayer.new | |
393 | + @f.player_id = "f" | |
394 | + @f.name = "f" | |
395 | + @f.win = 4000 | |
396 | + @f.loss = 4000 | |
397 | + @f.rate = 2150 | |
398 | + @g = ShogiServer::BasicPlayer.new | |
399 | + @g.player_id = "g" | |
400 | + @g.name = "g" | |
401 | + @g.win = 5000 | |
402 | + @g.loss = 5000 | |
403 | + @g.rate = 2500 | |
404 | + @h = ShogiServer::BasicPlayer.new | |
405 | + @h.player_id = "h" | |
406 | + @h.name = "h" | |
407 | + @h.win = 6000 | |
408 | + @h.loss = 6000 | |
409 | + @h.rate = 3000 | |
410 | + @x = ShogiServer::BasicPlayer.new | |
411 | + @x.player_id = "x" | |
412 | + @x.name = "x" | |
413 | + | |
414 | + $league.add(@a) | |
415 | + $league.add(@b) | |
416 | + $league.add(@c) | |
417 | + $league.add(@d) | |
418 | + $league.add(@e) | |
419 | + $league.add(@f) | |
420 | + $league.add(@g) | |
421 | + $league.add(@h) | |
422 | + $league.add(@x) | |
423 | + end | |
424 | + | |
425 | + def teardown | |
426 | + @file.delete if @file.exist? | |
427 | + end | |
428 | + | |
429 | + def assert_pairs(x_array, y_array) | |
430 | + if (x_array.size != y_array.size) | |
431 | + assert_equal(x_array.size, y_array.size) | |
432 | + return | |
433 | + end | |
434 | + i = 0 | |
435 | + | |
436 | + if (x_array.size == 1) | |
437 | + assert_equal(x_array[0].name, y_array[0].name) | |
438 | + return | |
439 | + end | |
440 | + | |
441 | + ret = true | |
442 | + while i < x_array.size | |
443 | + if i == x_array.size-1 | |
444 | + assert_equal(x_array[i].name, y_array[i].name) | |
445 | + break | |
446 | + end | |
447 | + px1 = x_array[i] | |
448 | + px2 = x_array[i+1] | |
449 | + py1 = y_array[i] | |
450 | + py2 = y_array[i+1] | |
451 | + | |
452 | + if ! ((px1.name == py1.name && px2.name == py2.name) || | |
453 | + (px1.name == py2.name && px2.name == py1.name)) | |
454 | + ret = false | |
455 | + end | |
456 | + i += 2 | |
457 | + end | |
458 | + | |
459 | + assert(ret) | |
460 | + end | |
461 | + | |
462 | + def test_match_one_player | |
463 | + players = [@a] | |
464 | + assert_equal(0, @pairing.calculate_diff_with_penalty(players,nil)) | |
465 | + r = @pairing.match(players) | |
466 | + assert_pairs([@a], r) | |
467 | + end | |
468 | + | |
469 | + def test_match_two_players | |
470 | + players = [@a,@b] | |
471 | + assert_equal(@b.rate-@a.rate, @pairing.calculate_diff_with_penalty([@a,@b],nil)) | |
472 | + assert_equal(@b.rate-@a.rate, @pairing.calculate_diff_with_penalty([@b,@a],nil)) | |
473 | + r = @pairing.match(players) | |
474 | + assert_pairs([@a,@b], r) | |
475 | + end | |
476 | + | |
477 | + def test_match_three_players | |
478 | + players = [@h,@a,@b] | |
479 | + assert_equal(300, @pairing.calculate_diff_with_penalty([@a,@b,@h],nil)) | |
480 | + assert_equal(2200, @pairing.calculate_diff_with_penalty([@b,@h,@a],nil)) | |
481 | + r = @pairing.match(players) | |
482 | + assert_pairs([@a,@b,@h], r) | |
483 | + assert_pairs([@a,@b,@h], players) | |
484 | + end | |
485 | + | |
486 | + def test_calculate_diff_with_penalty | |
487 | + players = [@a,@b] | |
488 | + assert_equal(@b.rate-@a.rate, @pairing.calculate_diff_with_penalty(players,nil)) | |
489 | + | |
490 | + dummy = nil | |
491 | + def @history.make_record(game_result) | |
492 | + {:game_id => "wdoor+floodgate-900-0-a-b-1", | |
493 | + :black => "b", :white => "a", | |
494 | + :winner => "a", :loser => "b"} | |
495 | + end | |
496 | + @history.update(dummy) | |
497 | + assert_equal(@b.rate-@a.rate+400, @pairing.calculate_diff_with_penalty(players, @history)) | |
498 | + end | |
499 | + | |
500 | + def test_calculate_diff_with_penalty2 | |
501 | + players = [@a,@b,@g,@h] | |
502 | + assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil)) | |
503 | + end | |
504 | + | |
505 | + def test_calculate_diff_with_penalty2_1 | |
506 | + players = [@a,@b,@g,@h] | |
507 | + assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil)) | |
508 | + dummy = nil | |
509 | + def @history.make_record(game_result) | |
510 | + {:game_id => "wdoor+floodgate-900-0-a-b-1", | |
511 | + :black => "b", :white => "a", | |
512 | + :winner => "a", :loser => "b"} | |
513 | + end | |
514 | + @history.update(dummy) | |
515 | + assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players, @history)) | |
516 | + end | |
517 | + | |
518 | + def test_calculate_diff_with_penalty2_2 | |
519 | + players = [@a,@b,@g,@h] | |
520 | + assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil)) | |
521 | + dummy = nil | |
522 | + def @history.make_record(game_result) | |
523 | + {:game_id => "wdoor+floodgate-900-0-a-b-1", | |
524 | + :black => "g", :white => "h", | |
525 | + :winner => "h", :loser => "g"} | |
526 | + end | |
527 | + @history.update(dummy) | |
528 | + assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players, @history)) | |
529 | + #assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate+400, @pairing.calculate_diff_with_penalty(players, [@b,@a,@h,@g])) | |
530 | + end | |
531 | + | |
532 | + def test_calculate_diff_with_penalty2_3 | |
533 | + players = [@a,@b,@g,@h] | |
534 | + assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil)) | |
535 | + dummy = nil | |
536 | + def @history.make_record(game_result) | |
537 | + {:game_id => "wdoor+floodgate-900-0-a-b-1", | |
538 | + :black => "g", :white => "h", | |
539 | + :winner => "h", :loser => "g"} | |
540 | + end | |
541 | + @history.update(dummy) | |
542 | + def @history.make_record(game_result) | |
543 | + {:game_id => "wdoor+floodgate-900-0-a-b-1", | |
544 | + :black => "b", :white => "a", | |
545 | + :winner => "a", :loser => "b"} | |
546 | + end | |
547 | + @history.update(dummy) | |
548 | + assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate+400, @pairing.calculate_diff_with_penalty(players, @history)) | |
549 | + end | |
550 | + | |
551 | + def test_get_player_rate_0 | |
552 | + assert_equal(2150, @pairing.get_player_rate(@x, @history)) | |
553 | + | |
554 | + dummy = nil | |
555 | + def @history.make_record(game_result) | |
556 | + {:game_id => "wdoor+floodgate-900-0-x-a-1", | |
557 | + :black => "x", :white => "a", | |
558 | + :winner => "x", :loser => "a"} | |
559 | + end | |
560 | + @history.update(dummy) | |
561 | + assert_equal(@a.rate+100, @pairing.get_player_rate(@x, @history)) | |
562 | + | |
563 | + def @history.make_record(game_result) | |
564 | + {:game_id => "wdoor+floodgate-900-0-x-b-1", | |
565 | + :black => "x", :white => "b", | |
566 | + :winner => "b", :loser => "x"} | |
567 | + end | |
568 | + @history.update(dummy) | |
569 | + | |
570 | + assert_equal((@a.rate+100+@b.rate-100)/2, @pairing.get_player_rate(@x, @history)) | |
571 | + end | |
572 | +end | |
330 | 573 |
@@ -0,0 +1,92 @@ | ||
1 | +$:.unshift File.join(File.dirname(__FILE__), "..") | |
2 | +require 'test/unit' | |
3 | +require 'test/mock_player' | |
4 | +require 'shogi_server/board' | |
5 | +require 'shogi_server/game' | |
6 | +require 'shogi_server/player' | |
7 | + | |
8 | +class DummyPlayer | |
9 | + def initialize(mytime) | |
10 | + @mytime = mytime | |
11 | + end | |
12 | + attr_reader :mytime | |
13 | +end | |
14 | + | |
15 | +class TestTimeClockFactor < Test::Unit::TestCase | |
16 | + def test_chess_clock | |
17 | + c = ShogiServer::TimeClock::factory(1, "hoge-900-0") | |
18 | + assert_instance_of(ShogiServer::ChessClock, c) | |
19 | + | |
20 | + c = ShogiServer::TimeClock::factory(1, "hoge-1500-60") | |
21 | + assert_instance_of(ShogiServer::ChessClock, c) | |
22 | + end | |
23 | + | |
24 | + def test_stop_watch_clock | |
25 | + c = ShogiServer::TimeClock::factory(1, "hoge-1500-060") | |
26 | + assert_instance_of(ShogiServer::StopWatchClock, c) | |
27 | + end | |
28 | +end | |
29 | + | |
30 | +class TestChessClock < Test::Unit::TestCase | |
31 | + def test_time_duration | |
32 | + tc = ShogiServer::ChessClock.new(1, 1500, 60) | |
33 | + assert_equal(1, tc.time_duration(100.1, 100.9)) | |
34 | + assert_equal(1, tc.time_duration(100, 101)) | |
35 | + assert_equal(1, tc.time_duration(100.1, 101.9)) | |
36 | + assert_equal(2, tc.time_duration(100.1, 102.9)) | |
37 | + assert_equal(2, tc.time_duration(100, 102)) | |
38 | + end | |
39 | + | |
40 | + def test_without_byoyomi | |
41 | + tc = ShogiServer::ChessClock.new(1, 1500, 0) | |
42 | + | |
43 | + p = DummyPlayer.new 100 | |
44 | + assert(!tc.timeout?(p, 100, 101)) | |
45 | + assert(!tc.timeout?(p, 100, 199)) | |
46 | + assert(tc.timeout?(p, 100, 200)) | |
47 | + assert(tc.timeout?(p, 100, 201)) | |
48 | + end | |
49 | + | |
50 | + def test_with_byoyomi | |
51 | + tc = ShogiServer::ChessClock.new(1, 1500, 60) | |
52 | + | |
53 | + p = DummyPlayer.new 100 | |
54 | + assert(!tc.timeout?(p, 100, 101)) | |
55 | + assert(!tc.timeout?(p, 100, 259)) | |
56 | + assert(tc.timeout?(p, 100, 260)) | |
57 | + assert(tc.timeout?(p, 100, 261)) | |
58 | + | |
59 | + p = DummyPlayer.new 30 | |
60 | + assert(!tc.timeout?(p, 100, 189)) | |
61 | + assert(tc.timeout?(p, 100, 190)) | |
62 | + end | |
63 | + | |
64 | + def test_with_byoyomi2 | |
65 | + tc = ShogiServer::ChessClock.new(1, 0, 60) | |
66 | + | |
67 | + p = DummyPlayer.new 0 | |
68 | + assert(!tc.timeout?(p, 100, 159)) | |
69 | + assert(tc.timeout?(p, 100, 160)) | |
70 | + end | |
71 | +end | |
72 | + | |
73 | +class TestStopWatchClock < Test::Unit::TestCase | |
74 | + def test_time_duration | |
75 | + tc = ShogiServer::StopWatchClock.new(1, 1500, 60) | |
76 | + assert_equal(0, tc.time_duration(100.1, 100.9)) | |
77 | + assert_equal(0, tc.time_duration(100, 101)) | |
78 | + assert_equal(0, tc.time_duration(100, 159.9)) | |
79 | + assert_equal(60, tc.time_duration(100, 160)) | |
80 | + assert_equal(60, tc.time_duration(100, 219)) | |
81 | + assert_equal(120, tc.time_duration(100, 220)) | |
82 | + end | |
83 | + | |
84 | + def test_with_byoyomi | |
85 | + tc = ShogiServer::StopWatchClock.new(1, 600, 60) | |
86 | + | |
87 | + p = DummyPlayer.new 60 | |
88 | + assert(!tc.timeout?(p, 100, 159)) | |
89 | + assert(tc.timeout?(p, 100, 160)) | |
90 | + end | |
91 | +end | |
92 | + |