ファイル情報

Rev. 2d5cda5e9dde331e37d58133b688a8b2c6295345
サイズ 22,415 バイト
日時 2017-09-03 14:40:39
作者 Daigo Moriwaki
ログメッセージ

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

内容

  1. #!/usr/bin/ruby
  2. # $Id$
  3. #
  4. # Author:: Daigo Moriwaki
  5. # Homepage:: http://sourceforge.jp/projects/shogi-server/
  6. #
  7. #--
  8. # Copyright (C) 2006-2012 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 game results files generated by the mk_game_results command,
  28. # calculates rating scores of each player, and then outputs a yaml file
  29. # (players.yaml) that Shogi-server can recognize.
  30. #
  31. # == Usage
  32. #
  33. # ./mk_rate [options] GAME_RESULTS_FILE [...]
  34. #
  35. # ./mk_rate [options]
  36. #
  37. # GAME_RESULTS_FILE::
  38. # a path to a file listing results of games, which is generated by the
  39. # mk_game_results command.
  40. # In the second style above, the file content can be read from the stdin.
  41. #
  42. # --abnormal-threshold::
  43. # n [plies] (default 30)
  44. # Games that end with the 'abnormal' status are counted in win/lost games
  45. # for the rating calculation if a game plays more than n plies. Otherwise
  46. # (or if n is zero), abnormal games are counted out of rating games.
  47. #
  48. # --base-date::
  49. # a base time point for this calculation (default now). Ex. '2009-10-31'
  50. #
  51. # --half-life::
  52. # n [days] (default 60)
  53. #
  54. # --half-life-ignore::
  55. # m [days] (default 7)
  56. # after m days, the half-life effect works
  57. #
  58. # --ignore::
  59. # m [days] (default 365*2)
  60. # old results will be ignored
  61. #
  62. # --fixed-rate-player::
  63. # player whose rate is fixed at the rate
  64. #
  65. # --fixed-rate::
  66. # rate
  67. #
  68. # --skip-draw-games::
  69. # skip draw games. [default: draw games are counted in as 0.5 win and 0.5
  70. # lost.]
  71. #
  72. # --help::
  73. # show this message
  74. #
  75. # == PREREQUIRE
  76. #
  77. # Sample Command lines that install prerequires will work on Debian.
  78. #
  79. # * Ruby 2.0.0 or later (including Rubygems)
  80. #
  81. # $ sudo aptitude install ruby
  82. #
  83. # * Ruby bindings for the GNU Scientific Library (GSL[http://rb-gsl.rubyforge.org/])
  84. #
  85. # $ sudo aptitude install ruby-gsl
  86. #
  87. # * RGL: {Ruby Graph Library}[http://rubyforge.org/projects/rgl/]
  88. #
  89. # $ sudo gem install rgl
  90. #
  91. # == Examples
  92. #
  93. # $ ./mk_rate game_results.txt > players.yaml
  94. #
  95. # $ ./mk_game_results . | ./mk_rate > players.yaml
  96. #
  97. # If you do not want the file to be update in case of errors,
  98. #
  99. # $ ./mk_rate game_results.txt && ./mk_rate game_results.txt > players.yaml
  100. #
  101. # == How players are rated
  102. #
  103. # The conditions that games and players are rated as following:
  104. #
  105. # * Rated games, which were played by both rated players.
  106. # * Rated players, who logged in the server with a name followed by a trip: "name,trip".
  107. # * (Rated) players, who played more than $GAMES_LIMIT [15] (rated) games.
  108. #
  109. require 'pathname'
  110. $:.unshift(File.dirname(Pathname.new(__FILE__).realpath))
  111. require 'utils/csa-filter'
  112. require 'yaml'
  113. require 'time'
  114. require 'getoptlong'
  115. require 'set'
  116. require 'rubygems'
  117. require 'gsl'
  118. require 'rgl/adjacency'
  119. require 'rgl/connected_components'
  120. #################################################
  121. # Constants
  122. #
  123. # Count out players who play less games than $GAMES_LIMIT
  124. $GAMES_LIMIT = $DEBUG ? 0 : 15
  125. WIN_MARK = "win"
  126. LOSS_MARK = "lose"
  127. DRAW_MARK = "draw"
  128. # Holds players
  129. $players = Hash.new
  130. # Holds the last time when a player gamed
  131. $players_time = Hash.new { Time.at(0) }
  132. # Holds history of input lines to check duplicated inputs
  133. $history = Set.new
  134. #################################################
  135. # Keeps the value of the lowest key
  136. #
  137. class Record
  138. def initialize
  139. @lowest = []
  140. end
  141. def set(key, value)
  142. if @lowest.empty? || key < @lowest[0]
  143. @lowest = [key, value]
  144. end
  145. end
  146. def get
  147. if @lowest.empty?
  148. nil
  149. else
  150. @lowest[1]
  151. end
  152. end
  153. end
  154. #################################################
  155. # Calculates rates of every player from a Win Loss GSL::Matrix
  156. #
  157. class Rating
  158. include Math
  159. # The model of the win possibility is 1/(1 + 10^(-d/400)).
  160. # The equation in this class is 1/(1 + e^(-Kd)).
  161. # So, K should be calculated like this.
  162. K = Math.log(10.0) / 400.0
  163. # Convergence limit to stop Newton method.
  164. ERROR_LIMIT = 1.0e-3
  165. # Stop Newton method after this iterations.
  166. COUNT_MAX = 500
  167. # Average rate among the players
  168. AVERAGE_RATE = 1000
  169. ###############
  170. # Class methods
  171. #
  172. ##
  173. # Calcurates the average of the vector.
  174. #
  175. def Rating.average(vector, mean=0.0)
  176. sum = Array(vector).inject(0.0) {|sum, n| sum + n}
  177. vector -= GSL::Vector[*Array.new(vector.size, sum/vector.size - mean)]
  178. vector
  179. end
  180. ##################
  181. # Instance methods
  182. #
  183. def initialize(win_loss_matrix)
  184. @record = Record.new
  185. @n = win_loss_matrix
  186. case @n
  187. when GSL::Matrix, GSL::Matrix::Int
  188. @size = @n.size1
  189. when ::Matrix
  190. @size = @n.row_size
  191. else
  192. raise ArgumentError
  193. end
  194. initial_rate
  195. end
  196. attr_reader :rate, :n
  197. def player_vector
  198. GSL::Vector[*
  199. (0...@size).collect {|k| yield k}
  200. ]
  201. end
  202. def each_player
  203. (0...@size).each {|k| yield k}
  204. end
  205. ##
  206. # The possibility that the player k will beet the player i.
  207. #
  208. def win_rate(k,i)
  209. 1.0/(1.0 + exp(@rate[i]-@rate[k]))
  210. end
  211. ##
  212. # Most possible equation
  213. #
  214. def func_vector
  215. player_vector do|k|
  216. sum = 0.0
  217. each_player do |i|
  218. next if i == k
  219. sum += @n[k,i] * win_rate(i,k) - @n[i,k] * win_rate(k,i)
  220. end
  221. sum * 2.0
  222. end
  223. end
  224. ##
  225. # / f0/R0 f0/R1 f0/R2 ... \
  226. # dfk/dRj = | f1/R0 f1/R1 f1/R2 ... |
  227. # \ f2/R0 f2/R1 f2/R2 ... /
  228. def d_func(k,j)
  229. sum = 0.0
  230. if k == j
  231. each_player do |i|
  232. next if i == k
  233. sum += win_rate(i,k) * win_rate(k,i) * (@n[k,i] + @n[i,k])
  234. end
  235. sum *= -2.0
  236. else # k != j
  237. sum = 2.0 * win_rate(j,k) * win_rate(k,j) * (@n[k,j] + @n[j,k])
  238. end
  239. sum
  240. end
  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. # The initial value of the rate, which is of very importance for Newton
  257. # method. This is based on my huristics; the higher the win probablity of
  258. # a player is, the greater points he takes.
  259. #
  260. def initial_rate
  261. possibility =
  262. player_vector do |k|
  263. v = GSL::Vector[0, 0]
  264. each_player do |i|
  265. next if k == i
  266. v += GSL::Vector[@n[k,i], @n[i,k]]
  267. end
  268. v.nrm2 < 1 ? 0 : v[0] / (v[0] + v[1])
  269. end
  270. rank = possibility.sort_index
  271. @rate = player_vector do |k|
  272. K*500 * (rank[k]+1) / @size
  273. end
  274. average!
  275. end
  276. ##
  277. # Resets @rate as the higher the current win probablity of a player is,
  278. # the greater points he takes.
  279. #
  280. def initial_rate2
  281. @rate = @record.get || @rate
  282. rank = @rate.sort_index
  283. @rate = player_vector do |k|
  284. K*@count*1.5 * (rank[k]+1) / @size
  285. end
  286. average!
  287. end
  288. # mu is the deaccelrating parameter in Deaccelerated Newton method
  289. def deaccelrate(mu, old_rate, a, old_f_nrm2)
  290. @rate = old_rate - a * mu
  291. if func_vector.nrm2 < (1 - mu / 4.0 ) * old_f_nrm2 then
  292. return
  293. end
  294. if mu < 1e-4
  295. @record.set(func_vector.nrm2, @rate)
  296. initial_rate2
  297. return
  298. end
  299. $stderr.puts "mu: %f " % [mu] if $DEBUG
  300. deaccelrate(mu*0.5, old_rate, a, old_f_nrm2)
  301. end
  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. # Main loop
  310. begin
  311. # Solve the equation:
  312. # J*a=f
  313. # @rate_(n+1) = @rate_(n) - a
  314. #
  315. # f.nrm2 should approach to zero.
  316. f = func_vector
  317. j = j_matrix
  318. # $stderr.puts "j: %s" % [j.inspect] if $DEBUG
  319. $stderr.puts "f: %s -> %f" % [f.to_a.inspect, f.nrm2] if $DEBUG
  320. # GSL::Linalg::LU.solve or GSL::Linalg::HH.solve would be available instead.
  321. #a = GSL::Linalg::HH.solve(j, f)
  322. a, = GSL::MultiFit::linear(j, f)
  323. a = self.class.average(a)
  324. # $stderr.puts "a: %s -> %f" % [a.to_a.inspect, a.nrm2] if $DEBUG
  325. # Deaccelerated Newton method
  326. # GSL::Vector object should be immutable.
  327. old_rate = @rate
  328. old_f = f
  329. old_f_nrm2 = old_f.nrm2
  330. deaccelrate(1.0, old_rate, a, old_f_nrm2)
  331. #@rate -= a # Instead, do not deaccelerate
  332. @record.set(func_vector.nrm2, @rate)
  333. $stderr.printf "|error| : %5.2e\n", a.nrm2 if $DEBUG
  334. @count += 1
  335. if @count > COUNT_MAX
  336. $stderr.puts "Values seem to oscillate. Stopped the process."
  337. $stderr.puts "f: %s -> %f" % [func_vector.to_a.inspect, func_vector.nrm2]
  338. break
  339. end
  340. end while (a.nrm2 > ERROR_LIMIT * @rate.nrm2)
  341. @rate = @record.get
  342. $stderr.puts "resolved f: %s -> %f" %
  343. [func_vector.to_a.inspect, func_vector.nrm2] if $DEBUG
  344. $stderr.puts "Count: %d" % [@count] if $DEBUG
  345. @rate *= 1.0/K
  346. finite!
  347. self
  348. end
  349. ##
  350. # Make the values of @rate finite.
  351. #
  352. def finite!
  353. @rate = @rate.collect do |a|
  354. if a.infinite?
  355. a.infinite? * AVERAGE_RATE * 100
  356. else
  357. a
  358. end
  359. end
  360. end
  361. ##
  362. # Flatten the values of @rate.
  363. #
  364. def average!(mean=0.0)
  365. @rate = self.class.average(@rate, mean)
  366. end
  367. ##
  368. # Translate by value
  369. #
  370. def translate!(value)
  371. @rate += value
  372. end
  373. ##
  374. # Make the values of @rate integer.
  375. #
  376. def integer!
  377. @rate = @rate.collect do |a|
  378. if a.finite?
  379. a.to_i
  380. elsif a.nan?
  381. 0
  382. elsif a.infinite?
  383. a.infinite? * AVERAGE_RATE * 100
  384. end
  385. end
  386. end
  387. end
  388. #################################################
  389. # Encapsulate a pair of keys and win loss matrix.
  390. # - keys is an array of player IDs; [gps+123, foo+234, ...]
  391. # - matrix holds games # where player i (row index) beats player j (column index).
  392. # The row and column indexes match with the keys.
  393. #
  394. # This object should be immutable. If an internal state is being modified, a
  395. # new object is always returned.
  396. #
  397. class WinLossMatrix
  398. ###############
  399. # Class methods
  400. #
  401. def self.mk_matrix(players)
  402. keys = players.keys.sort
  403. size = keys.size
  404. matrix =
  405. GSL::Matrix[*
  406. ((0...size).collect do |k|
  407. p1 = keys[k]
  408. p1_hash = players[p1]
  409. ((0...size).collect do |j|
  410. if k == j
  411. 0
  412. else
  413. p2 = keys[j]
  414. v = p1_hash[p2] || GSL::Vector[0,0]
  415. v[0]
  416. end
  417. end)
  418. end)]
  419. return WinLossMatrix.new(keys, matrix)
  420. end
  421. def self.mk_win_loss_matrix(players)
  422. obj = mk_matrix(players)
  423. return obj.filter
  424. end
  425. ##################
  426. # Instance methods
  427. #
  428. # an array of player IDs; [gps+123, foo+234, ...]
  429. attr_reader :keys
  430. # matrix holds games # where player i (row index) beats player j (column index).
  431. # The row and column indexes match with the keys.
  432. attr_reader :matrix
  433. def initialize(keys, matrix)
  434. @keys = keys
  435. @matrix = matrix
  436. end
  437. ##
  438. # Returns the size of the keys/matrix
  439. #
  440. def size
  441. if @keys
  442. @keys.size
  443. else
  444. nil
  445. end
  446. end
  447. ##
  448. # Removes players in a rows such as [1,3,5], and then returns a new
  449. # object.
  450. #
  451. def delete_rows(rows)
  452. rows = rows.sort.reverse
  453. copied_cols = []
  454. (0...size).each do |i|
  455. next if rows.include?(i)
  456. row = @matrix.row(i).clone
  457. rows.each do |j|
  458. row.delete_at(j)
  459. end
  460. copied_cols << row
  461. end
  462. if copied_cols.size == 0
  463. new_matrix = GSL::Matrix.new
  464. else
  465. new_matrix = GSL::Matrix[*copied_cols]
  466. end
  467. new_keys = @keys.clone
  468. rows.each do |j|
  469. new_keys.delete_at(j)
  470. end
  471. return WinLossMatrix.new(new_keys, new_matrix)
  472. end
  473. ##
  474. # Removes players who do not pass a criteria to be rated, and returns a
  475. # new object.
  476. #
  477. def filter
  478. $stderr.puts @keys.inspect if $DEBUG
  479. $stderr.puts @matrix.inspect if $DEBUG
  480. delete = []
  481. (0...size).each do |i|
  482. row = @matrix.row(i)
  483. col = @matrix.col(i)
  484. win = row.sum
  485. loss = col.sum
  486. if win < 1 || loss < 1 || win + loss < $GAMES_LIMIT
  487. delete << i
  488. end
  489. end
  490. # The recursion ends if there is nothing to delete
  491. return self if delete.empty?
  492. new_obj = delete_rows(delete)
  493. new_obj.filter
  494. end
  495. ##
  496. # Cuts self into connecting groups such as each player in a group has at least
  497. # one game with other players in the group. Returns them as an array.
  498. #
  499. def connected_subsets
  500. g = RGL::AdjacencyGraph.new
  501. (0...size).each do |k|
  502. (0...size).each do |i|
  503. next if k == i
  504. if @matrix[k,i] > 0
  505. g.add_edge(k,i)
  506. end
  507. end
  508. end
  509. subsets = []
  510. g.each_connected_component do |c|
  511. new_keys = []
  512. c.each do |v|
  513. new_keys << keys[v.to_s.to_i]
  514. end
  515. subsets << new_keys
  516. end
  517. subsets = subsets.sort {|a,b| b.size <=> a.size}
  518. result = subsets.collect do |keys|
  519. matrix =
  520. GSL::Matrix[*
  521. ((0...keys.size).collect do |k|
  522. p1 = @keys.index(keys[k])
  523. ((0...keys.size).collect do |j|
  524. if k == j
  525. 0
  526. else
  527. p2 = @keys.index(keys[j])
  528. @matrix[p1,p2]
  529. end
  530. end)
  531. end)]
  532. WinLossMatrix.new(keys, matrix)
  533. end
  534. return result
  535. end
  536. def to_s
  537. "size : #{@keys.size}" + "\n" +
  538. @keys.inspect + "\n" +
  539. @matrix.inspect
  540. end
  541. end
  542. #################################################
  543. # Main methods
  544. #
  545. # Half-life effect
  546. # After NHAFE_LIFE days value will get half.
  547. # 0.693 is constant, where exp(0.693) ~ 0.5
  548. def half_life(days)
  549. if days < $options["half-life-ignore"]
  550. return 1.0
  551. else
  552. Math::exp(-0.693/$options["half-life"]*(days-$options["half-life-ignore"]))
  553. end
  554. end
  555. def _add_win_loss(winner, loser, time)
  556. how_long_days = ($options["base-date"] - time)/(3600*24)
  557. $players[winner] ||= Hash.new { GSL::Vector[0,0] }
  558. $players[loser] ||= Hash.new { GSL::Vector[0,0] }
  559. $players[winner][loser] += GSL::Vector[1.0*half_life(how_long_days),0]
  560. $players[loser][winner] += GSL::Vector[0,1.0*half_life(how_long_days)]
  561. end
  562. def _add_draw(player1, player2, time)
  563. how_long_days = ($options["base-date"] - time)/(3600*24)
  564. $players[player1] ||= Hash.new { GSL::Vector[0,0] }
  565. $players[player2] ||= Hash.new { GSL::Vector[0,0] }
  566. $players[player1][player2] += GSL::Vector[0.5*half_life(how_long_days),0.5*half_life(how_long_days)]
  567. $players[player2][player1] += GSL::Vector[0.5*half_life(how_long_days),0.5*half_life(how_long_days)]
  568. end
  569. def _add_time(player, time)
  570. $players_time[player] = time if $players_time[player] < time
  571. end
  572. def add(black_mark, black_name, white_name, white_mark, time)
  573. if black_mark == WIN_MARK && white_mark == LOSS_MARK
  574. _add_win_loss(black_name, white_name, time)
  575. elsif black_mark == LOSS_MARK && white_mark == WIN_MARK
  576. _add_win_loss(white_name, black_name, time)
  577. elsif black_mark == DRAW_MARK && white_mark == DRAW_MARK
  578. if $options["skip-draw-games"]
  579. return
  580. else
  581. _add_draw(black_name, white_name, time)
  582. end
  583. else
  584. raise "Never reached!"
  585. end
  586. _add_time(black_name, time)
  587. _add_time(white_name, time)
  588. end
  589. def identify_id(id)
  590. if /@NORATE\+/ =~ id # the player having @NORATE in the name should not be rated
  591. return nil
  592. end
  593. id.gsub(/@.*?\+/,"+")
  594. end
  595. # Parse a game result line
  596. #
  597. def parse(line)
  598. if $history.include? line
  599. $stderr.puts "[WARNING] Duplicated: #{line}"
  600. return
  601. end
  602. $history.add line
  603. time, state, black_mark, black_id, white_id, white_mark, file, moves = line.split("\t")
  604. unless time && state && black_mark && black_id &&
  605. white_id && white_mark && file
  606. $stderr.puts "Failed to parse the line : #{line}"
  607. return
  608. end
  609. if state == "abnormal"
  610. csa = CsaFileReader.new(file, "EUC-JP")
  611. if $options["abnormal-threshold"] == 0 || csa.ply <= $options["abnormal-threshold"]
  612. return
  613. end
  614. end
  615. time = Time.parse(time)
  616. return if $options["base-date"] < time
  617. how_long_days = ($options["base-date"] - time)/(3600*24)
  618. if (how_long_days > $options["ignore"])
  619. return
  620. end
  621. black_id = identify_id(black_id)
  622. white_id = identify_id(white_id)
  623. if black_id && white_id && (black_id != white_id) &&
  624. black_mark && white_mark
  625. add(black_mark, black_id, white_id, white_mark, time)
  626. end
  627. end
  628. def validate(yaml)
  629. yaml["players"].each do |group_key, group|
  630. group.each do |player_key, player|
  631. rate = player['rate']
  632. next unless rate
  633. if rate > 10000 || rate < -10000
  634. return false
  635. end
  636. end
  637. end
  638. return true
  639. end
  640. def usage(io)
  641. io.puts <<EOF
  642. USAGE: #{$0} [options] GAME_RESULTS_FILE [...]
  643. #{$0} [options]
  644. GAME_RESULTS_FILE:
  645. a path to a file listing results of games, which is genrated by the
  646. mk_game_results command.
  647. In the second style above, the file content can be read from the stdin.
  648. OPTOINS:
  649. --base-date a base time point for this calicuration (default now). Ex. '2009-10-31'
  650. --half-life n [days] (default 60)
  651. --half-life-ignore m [days] (default 7)
  652. after m days, half-life effect works
  653. --ignore n [days] (default 730 [=365*2]).
  654. Results older than n days from the 'base-date' are ignored.
  655. --fixed-rate-player player whose rate is fixed at the rate
  656. --fixed-rate rate
  657. --skip-draw-games skip draw games. [default: draw games are counted in
  658. as 0.5 win and 0.5 lost]
  659. --help show this message
  660. EOF
  661. end
  662. def main
  663. $options = Hash::new
  664. parser = GetoptLong.new(
  665. ["--abnormal-threshold", GetoptLong::REQUIRED_ARGUMENT],
  666. ["--base-date", GetoptLong::REQUIRED_ARGUMENT],
  667. ["--half-life", GetoptLong::REQUIRED_ARGUMENT],
  668. ["--half-life-ignore", GetoptLong::REQUIRED_ARGUMENT],
  669. ["--help", "-h", GetoptLong::NO_ARGUMENT],
  670. ["--ignore", GetoptLong::REQUIRED_ARGUMENT],
  671. ["--fixed-rate-player", GetoptLong::REQUIRED_ARGUMENT],
  672. ["--fixed-rate", GetoptLong::REQUIRED_ARGUMENT],
  673. ["--skip-draw-games", GetoptLong::NO_ARGUMENT])
  674. parser.quiet = true
  675. begin
  676. parser.each_option do |name, arg|
  677. name.sub!(/^--/, '')
  678. $options[name] = arg.dup
  679. end
  680. if ( $options["fixed-rate-player"] && !$options["fixed-rate"]) ||
  681. (!$options["fixed-rate-player"] && $options["fixed-rate"]) ||
  682. ( $options["fixed-rate-player"] && $options["fixed-rate"].to_i <= 0)
  683. usage($stderr)
  684. exit 1
  685. end
  686. rescue
  687. usage($stderr)
  688. raise parser.error_message
  689. end
  690. if $options["help"]
  691. usage($stdout)
  692. exit 0
  693. end
  694. if $options["base-date"]
  695. $options["base-date"] = Time::parse $options["base-date"]
  696. else
  697. $options["base-date"] = Time.now
  698. end
  699. $options["abnormal-threshold"] ||= 30
  700. $options["abnormal-threshold"] = $options["abnormal-threshold"].to_i
  701. $options["half-life"] ||= 60
  702. $options["half-life"] = $options["half-life"].to_i
  703. $options["half-life-ignore"] ||= 7
  704. $options["half-life-ignore"] = $options["half-life-ignore"].to_i
  705. $options["ignore"] ||= 365*2
  706. $options["ignore"] = $options["ignore"].to_i
  707. $options["fixed-rate"] = $options["fixed-rate"].to_i if $options["fixed-rate"]
  708. if ARGV.empty?
  709. while line = $stdin.gets do
  710. parse line.strip
  711. end
  712. else
  713. while file = ARGV.shift do
  714. File.open(file) do |f|
  715. f.each_line do |line|
  716. parse line.strip
  717. end
  718. end
  719. end
  720. end
  721. yaml = {}
  722. yaml["players"] = {}
  723. rating_group = 0
  724. if $players.size > 0
  725. obj = WinLossMatrix::mk_win_loss_matrix($players)
  726. obj.connected_subsets.each do |win_loss_matrix|
  727. yaml["players"][rating_group] = {}
  728. rating = Rating.new(win_loss_matrix.matrix)
  729. rating.rating
  730. rating.average!(Rating::AVERAGE_RATE)
  731. rating.integer!
  732. if $options["fixed-rate-player"]
  733. # first, try exact match
  734. index = win_loss_matrix.keys.index($options["fixed-rate-player"])
  735. # second, try regular match
  736. unless index
  737. win_loss_matrix.keys.each_with_index do |p, i|
  738. if %r!#{$options["fixed-rate-player"]}! =~ p
  739. index = i
  740. end
  741. end
  742. end
  743. if index
  744. the_rate = rating.rate[index]
  745. rating.translate!($options["fixed-rate"] - the_rate)
  746. end
  747. end
  748. win_loss_matrix.keys.each_with_index do |p, i| # player_id, index#
  749. win = win_loss_matrix.matrix.row(i).sum
  750. loss = win_loss_matrix.matrix.col(i).sum
  751. yaml["players"][rating_group][p] =
  752. { 'name' => p.split("+")[0],
  753. 'rating_group' => rating_group,
  754. 'rate' => rating.rate[i],
  755. 'last_modified' => $players_time[p].dup,
  756. 'win' => win,
  757. 'loss' => loss}
  758. end
  759. rating_group += 1
  760. end
  761. end
  762. rating_group -= 1
  763. non_rated_group = 999 # large enough
  764. yaml["players"][non_rated_group] = {}
  765. $players.each_key do |id|
  766. # skip players who have already been rated
  767. found = false
  768. (0..rating_group).each do |i|
  769. found = true if yaml["players"][i][id]
  770. break if found
  771. end
  772. next if found
  773. v = GSL::Vector[0, 0]
  774. $players[id].each_value {|value| v += value}
  775. next if v[0] < 1 && v[1] < 1
  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. if __FILE__ == $0
  792. main
  793. end
  794. # vim: ts=2 sw=2 sts=0
旧リポジトリブラウザで表示