• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

shogi-server source


Commit MetaInfo

Revisão09bf2bfc8b6a996083cb3283259caf364a71a356 (tree)
Hora2013-11-22 21:47:59
AutorDaigo Moriwaki <daigo@debi...>
CommiterDaigo Moriwaki

Mensagem de Log

Merge remote-tracking branch 'origin/wdoor-stable'

Conflicts:
changelog

Mudança Sumário

Diff

--- a/changelog
+++ b/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+
176 2012-12-28 Daigo Moriwaki <daigo at debian dot org>
277
378 * [shogi-server]
--- a/mk_game_results
+++ b/mk_game_results
@@ -89,6 +89,7 @@ def grep(file)
8989 puts [time, state, black_mark, black_id, white_id, white_mark, file].join("\t")
9090 end
9191 end
92+ $stdout.flush
9293 end
9394
9495 # Show Usage
--- a/mk_rate
+++ b/mk_rate
@@ -49,6 +49,10 @@
4949 # m [days] (default 7)
5050 # after m days, the half-life effect works
5151 #
52+# --ignore::
53+# m [days] (default 365*2)
54+# old results will be ignored
55+#
5256 # --fixed-rate-player::
5357 # player whose rate is fixed at the rate
5458 #
@@ -660,6 +664,11 @@ def parse(line)
660664 return if state == "abnormal"
661665 time = Time.parse(time)
662666 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+
663672 black_id = identify_id(black_id)
664673 white_id = identify_id(white_id)
665674
@@ -697,6 +706,8 @@ OPTOINS:
697706 --half-life n [days] (default 60)
698707 --half-life-ignore m [days] (default 7)
699708 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.
700711 --fixed-rate-player player whose rate is fixed at the rate
701712 --fixed-rate rate
702713 --skip-draw-games skip draw games. [default: draw games are counted in
@@ -712,6 +723,7 @@ def main
712723 ["--half-life", GetoptLong::REQUIRED_ARGUMENT],
713724 ["--half-life-ignore", GetoptLong::REQUIRED_ARGUMENT],
714725 ["--help", "-h", GetoptLong::NO_ARGUMENT],
726+ ["--ignore", GetoptLong::REQUIRED_ARGUMENT],
715727 ["--fixed-rate-player", GetoptLong::REQUIRED_ARGUMENT],
716728 ["--fixed-rate", GetoptLong::REQUIRED_ARGUMENT],
717729 ["--skip-draw-games", GetoptLong::NO_ARGUMENT])
@@ -744,6 +756,8 @@ def main
744756 $options["half-life"] = $options["half-life"].to_i
745757 $options["half-life-ignore"] ||= 7
746758 $options["half-life-ignore"] = $options["half-life-ignore"].to_i
759+ $options["ignore"] ||= 365*2
760+ $options["ignore"] = $options["ignore"].to_i
747761 $options["fixed-rate"] = $options["fixed-rate"].to_i if $options["fixed-rate"]
748762
749763 if ARGV.empty?
--- /dev/null
+++ b/mk_rate-from-grep
@@ -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
--- /dev/null
+++ b/mk_rate-grep
@@ -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
--- a/shogi-server
+++ b/shogi-server
@@ -143,9 +143,6 @@ LICENSE
143143
144144 SEE ALSO
145145
146-RELEASE
147- #{ShogiServer::Release}
148-
149146 REVISION
150147 #{ShogiServer::Revision}
151148
--- a/shogi_server.rb
+++ b/shogi_server.rb
@@ -51,8 +51,7 @@ Default_Game_Name = "default-1500-0"
5151 One_Time = 10
5252 Least_Time_Per_Move = 1
5353 Login_Time = 300 # time for LOGIN
54-Release = "$Id$"
55-Revision = (r = /Revision: (\d+)/.match("$Revision$") ? r[1] : 0)
54+Revision = "20131104"
5655
5756 RELOAD_FILES = ["shogi_server/league/floodgate.rb",
5857 "shogi_server/league/persistent.rb",
--- a/shogi_server/board.rb
+++ b/shogi_server/board.rb
@@ -43,17 +43,42 @@ EOF
4343
4444 # Split a moves line into an array of a move string.
4545 # 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"]]
4850 #
4951 def Board.split_moves(moves)
5052 ret = []
5153
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
5782
5883 return ret
5984 end
@@ -242,14 +267,21 @@ EOF
242267
243268 # Set up a board starting with a position after the moves.
244269 # 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"]]
246272 #
247273 def set_from_moves(moves)
248274 initial()
249275 return :normal if moves.empty?
250276 rt = nil
251277 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
253285 raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
254286 end
255287 @initial_moves = moves.dup
--- a/shogi_server/buoy.rb
+++ b/shogi_server/buoy.rb
@@ -21,7 +21,7 @@ module ShogiServer
2121 end
2222
2323 def ==(rhs)
24- return (@game_name == rhs.game_name &&
24+ return (@game_name == rhs.game_name &&
2525 @moves == rhs.moves &&
2626 @owner == rhs.owner &&
2727 @count == rhs.count)
--- a/shogi_server/command.rb
+++ b/shogi_server/command.rb
@@ -69,6 +69,9 @@ module ShogiServer
6969 my_sente_str = $3
7070 cmd = GameChallengeCommand.new(str, player,
7171 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)
7275 when /^%%CHAT\s+(.+)/
7376 message = $1
7477 cmd = ChatCommand.new(str, player, message, $league.players)
@@ -94,6 +97,19 @@ module ShogiServer
9497 when /^%%GETBUOYCOUNT\s+(\S+)/
9598 game_name = $1
9699 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)
97113 when /^\s*$/
98114 cmd = SpaceCommand.new(str, player)
99115 when /^%%%[^%]/
@@ -481,7 +497,16 @@ module ShogiServer
481497
482498 def call
483499 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
485510 return :continue
486511 elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
487512 ## continue
@@ -666,9 +691,9 @@ module ShogiServer
666691 # Command for an error
667692 #
668693 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"
672697 end
673698 attr_reader :msg
674699
@@ -676,7 +701,7 @@ module ShogiServer
676701 cmd = @str.chomp
677702 # Aim to hide a possible password
678703 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]
680705 @player.write_safe(@msg)
681706 log_error(@msg)
682707 return :continue
@@ -810,4 +835,69 @@ module ShogiServer
810835 end
811836 end
812837
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+
813903 end # module ShogiServer
--- a/shogi_server/game.rb
+++ b/shogi_server/game.rb
@@ -19,6 +19,7 @@
1919
2020 require 'shogi_server/league/floodgate'
2121 require 'shogi_server/game_result'
22+require 'shogi_server/time_clock'
2223 require 'shogi_server/util'
2324
2425 module ShogiServer # for a namespace
@@ -69,6 +70,8 @@ class Game
6970 if (@game_name =~ /-(\d+)-(\d+)$/)
7071 @total_time = $1.to_i
7172 @byoyomi = $2.to_i
73+
74+ @time_clock = TimeClock::factory(Least_Time_Per_Move, @game_name)
7275 end
7376
7477 if (player0.sente)
@@ -87,7 +90,16 @@ class Game
8790 @sente.game = self
8891 @gote.game = self
8992
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
91103 @current_turn = @board.initial_moves.size
92104
93105 @sente.status = "agree_waiting"
@@ -110,6 +122,7 @@ class Game
110122 $league.games[@game_id] = self
111123
112124 log_message(sprintf("game created %s", @game_id))
125+ log_message(" " + @time_clock.to_s)
113126
114127 @start_time = nil
115128 @fh = open(@logfile, "w")
@@ -118,7 +131,7 @@ class Game
118131
119132 propose
120133 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
122135 attr_accessor :last_move, :current_turn
123136 attr_reader :result, :prepared_time
124137
@@ -218,22 +231,16 @@ class Game
218231 return nil
219232 end
220233
221- finish_flag = true
222234 @end_time = end_time
223- t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
224-
235+ finish_flag = true
225236 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))
228239 status = :timeout
229240 elsif (str == :timeout)
230241 return false # time isn't expired. players aren't swapped. continue game
231242 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)
237244 move_status = @board.handle_one_move(str, @sente == @current_player)
238245 # log_debug("move_status: %s for %s's %s" % [move_status, @sente == @current_player ? "BLACK" : "WHITE", str])
239246
@@ -332,8 +339,14 @@ class Game
332339 unless @board.initial_moves.empty?
333340 @fh.puts "'buoy game starting with %d moves" % [@board.initial_moves.size]
334341 @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
337350 end
338351 end
339352 end
@@ -351,7 +364,7 @@ Name-:#{@gote.name}
351364 Rematch_On_Draw:NO
352365 To_Move:+
353366 BEGIN Time
354-Time_Unit:1sec
367+Time_Unit:#{@time_clock.time_unit}
355368 Total_Time:#{@total_time}
356369 Byoyomi:#{@byoyomi}
357370 Least_Time_Per_Move:#{Least_Time_Per_Move}
@@ -385,14 +398,21 @@ Your_Turn:#{sg_flag}
385398 Rematch_On_Draw:NO
386399 To_Move:#{@board.teban ? "+" : "-"}
387400 BEGIN Time
388-Time_Unit:1sec
401+Time_Unit:#{@time_clock.time_unit}
389402 Total_Time:#{@total_time}
390403 Byoyomi:#{@byoyomi}
391404 Least_Time_Per_Move:#{Least_Time_Per_Move}
392405 END Time
393406 BEGIN Position
394407 #{@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")}
396416 END Position
397417 END Game_Summary
398418 EOM
@@ -408,6 +428,21 @@ EOM
408428
409429 return false
410430 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
411446
412447 private
413448
--- a/shogi_server/league/floodgate.rb
+++ b/shogi_server/league/floodgate.rb
@@ -257,6 +257,18 @@ class League
257257 return rc[:loser] == player_id
258258 end
259259
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+
260272 def last_valid_game(player_id)
261273 records = nil
262274 @@mutex.synchronize do
@@ -269,6 +281,28 @@ class League
269281 end
270282 return rc
271283 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
272306 end # class History
273307
274308
--- a/shogi_server/pairing.rb
+++ b/shogi_server/pairing.rb
@@ -25,7 +25,7 @@ module ShogiServer
2525
2626 class << self
2727 def default_factory
28- return swiss_pairing
28+ return least_diff_pairing
2929 end
3030
3131 def sort_by_rate_with_randomness
@@ -52,6 +52,14 @@ module ShogiServer
5252 StartGameWithoutHumans.new]
5353 end
5454
55+ def least_diff_pairing
56+ return [LogPlayers.new,
57+ ExcludeSacrificeGps500.new,
58+ MakeEven.new,
59+ LeastDiff.new,
60+ StartGameWithoutHumans.new]
61+ end
62+
5563 def match(players)
5664 logics = default_factory
5765 logics.inject(players) do |result, item|
@@ -62,6 +70,10 @@ module ShogiServer
6270 end # class << self
6371
6472
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+ #
6577 def match(players)
6678 # to be implemented
6779 log_message("Floodgate: %s" % [self.class.to_s])
@@ -232,17 +244,18 @@ module ShogiServer
232244 end
233245
234246 class SortByRateWithRandomness < Pairing
235- def initialize(rand1, rand2)
247+ def initialize(rand1, rand2, desc=false)
236248 super()
237249 @rand1, @rand2 = rand1, rand2
250+ @desc = desc
238251 end
239252
240- def match(players, desc=false)
253+ def match(players)
241254 super(players)
242255 cur_rate = Hash.new
243256 players.each{|a| cur_rate[a] = a.rate ? a.rate + rand(@rand1) : rand(@rand2)}
244257 players.sort!{|a,b| cur_rate[a] <=> cur_rate[b]}
245- players.reverse! if desc
258+ players.reverse! if @desc
246259 log_players(players) do |one|
247260 "%s %d (+ randomness %d)" % [one.name, one.rate, cur_rate[one] - one.rate]
248261 end
@@ -267,12 +280,12 @@ module ShogiServer
267280 rest = players - winners
268281
269282 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)
272285
273286 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)
276289
277290 players.clear
278291 [winners, rest].each do |group|
@@ -364,4 +377,126 @@ module ShogiServer
364377 end
365378 end
366379
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+
367502 end # ShogiServer
--- /dev/null
+++ b/shogi_server/time_clock.rb
@@ -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
--- a/test/TC_ALL.rb
+++ b/test/TC_ALL.rb
@@ -10,6 +10,7 @@ require 'TC_floodgate'
1010 require 'TC_floodgate_history'
1111 require 'TC_floodgate_next_time_generator'
1212 require 'TC_floodgate_thread.rb'
13+require 'TC_fork'
1314 require 'TC_functional'
1415 require 'TC_game'
1516 require 'TC_game_result'
@@ -24,6 +25,7 @@ require 'TC_oute_sennichite'
2425 require 'TC_pairing'
2526 require 'TC_player'
2627 require 'TC_rating'
28+require 'TC_time_clock'
2729 require 'TC_uchifuzume'
2830 require 'TC_usi'
2931 require 'TC_util'
--- a/test/TC_command.rb
+++ b/test/TC_command.rb
@@ -228,6 +228,16 @@ class TestFactoryMethod < Test::Unit::TestCase
228228 assert_instance_of(ShogiServer::GetBuoyCountCommand, cmd)
229229 end
230230
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+
231241 def test_void_command
232242 cmd = ShogiServer::Command.factory("%%%HOGE", @p)
233243 assert_instance_of(ShogiServer::VoidCommand, cmd)
@@ -237,29 +247,29 @@ class TestFactoryMethod < Test::Unit::TestCase
237247 cmd = ShogiServer::Command.factory("should_be_error", @p)
238248 assert_instance_of(ShogiServer::ErrorCommand, cmd)
239249 cmd.call
240- assert_match /unknown command should_be_error/, cmd.msg
250+ assert_match /unknown command: should_be_error/, cmd.msg
241251 end
242252
243253 def test_error_login
244254 cmd = ShogiServer::Command.factory("LOGIN hoge foo", @p)
245255 assert_instance_of(ShogiServer::ErrorCommand, cmd)
246256 cmd.call
247- assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
257+ assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
248258
249259 cmd = ShogiServer::Command.factory("LOGin hoge foo", @p)
250260 assert_instance_of(ShogiServer::ErrorCommand, cmd)
251261 cmd.call
252- assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
262+ assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
253263
254264 cmd = ShogiServer::Command.factory("LOGIN hoge foo", @p)
255265 assert_instance_of(ShogiServer::ErrorCommand, cmd)
256266 cmd.call
257- assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
267+ assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
258268
259269 cmd = ShogiServer::Command.factory("LOGINhoge foo", @p)
260270 assert_instance_of(ShogiServer::ErrorCommand, cmd)
261271 cmd.call
262- assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
272+ assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
263273 end
264274 end
265275
@@ -939,6 +949,28 @@ end
939949
940950 #
941951 #
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+#
942974 class TestGetBuoyCountCommand < BaseTestBuoyCommand
943975 def test_call
944976 buoy_game = ShogiServer::BuoyGame.new("buoy_testdeletebuoy-1500-0", "+7776FU", @p.name, 1)
@@ -1051,4 +1083,3 @@ class TestMonitorHandler2 < Test::Unit::TestCase
10511083 @player.out.join)
10521084 end
10531085 end
1054-
--- a/test/TC_floodgate.rb
+++ b/test/TC_floodgate.rb
@@ -395,6 +395,22 @@ class TestFloodgateHistory < Test::Unit::TestCase
395395 assert !@history.last_win?("foo")
396396 assert !@history.last_lose?("hoge")
397397 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])
398414 end
399415 end
400416
--- /dev/null
+++ b/test/TC_fork.rb
@@ -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
--- a/test/TC_pairing.rb
+++ b/test/TC_pairing.rb
@@ -1,11 +1,11 @@
11 $:.unshift File.join(File.dirname(__FILE__), "..")
22 require 'test/unit'
33 require 'shogi_server'
4+require 'shogi_server/league.rb'
45 require 'shogi_server/player'
56 require 'shogi_server/pairing'
67 require 'test/mock_log_message'
78
8-
99 def same_pair?(a, b)
1010 unless a.size == 2 && b.size == 2
1111 return false
@@ -327,4 +327,247 @@ class TestStartGameWithoutHumans < Test::Unit::TestCase
327327 end
328328 end
329329
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
330573
--- /dev/null
+++ b/test/TC_time_clock.rb
@@ -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+