• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
Keine 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

Revision24fee2243d10ec025357d52d8034cbc1dcf48338 (tree)
Zeit2013-03-20 17:23:36
AutorDaigo Moriwaki <beatles@user...>
CommiterDaigo Moriwaki

Log Message

[shogi-server] New pairing algorithm: ShogiServer::Pairing::LeastDiff

This pairing algorithm aims to minimize the total differences of
matching players' rates. It also includes penalyties when a match
is same as the previous one or a match is between human players.

It is based on a discussion with Yamashita-san on
http://www.sgtpepper.net/kaneko/diary/20120511.html.

Ändern Zusammenfassung

Diff

--- a/changelog
+++ b/changelog
@@ -1,3 +1,13 @@
1+2013-03-20 Daigo Moriwaki <daigo at debian dot org>
2+
3+ * [shogi-server]
4+ - New pairing algorithm: ShogiServer::Pairing::LeastDiff
5+ This pairing algorithm aims to minimize the total differences of
6+ matching players' rates. It also includes penalyties when a match
7+ is same as the previous one or a match is between human players.
8+ It is based on a discussion with Yamashita-san on
9+ http://www.sgtpepper.net/kaneko/diary/20120511.html.
10+
111 2012-12-30 Daigo Moriwaki <daigo at debian dot org>
212
313 * [shogi-server]
--- 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,137 @@ 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+ def average_rate(players)
392+ n=0
393+ sum=0
394+ players.find_all{|p| p.rate}.each do |p|
395+ n += 1
396+ sum += p.rate
397+ end
398+
399+ return n > 0 ? sum/n : 2150 # interger
400+ end
401+
402+ # Returns a player's rate value.
403+ # 1. If it has a valid rate, return the rate.
404+ # 2. If it has no valid rate, return average of the following values:
405+ # a. For games it won, the opponent's rate + 100
406+ # b. For games it lost, the opponent's rate - 100
407+ # (if the opponent has no valid rate, count out the game)
408+ # (if there are not such games, return 2150 (default value)
409+ #
410+ def get_player_rate(player, history)
411+ return player.rate if player.rate != 0
412+ return 2150 unless history
413+
414+ count = 0
415+ sum = 0
416+
417+ history.win_games(player.player_id).each do |g|
418+ next unless g[:loser]
419+ name = g[:loser].split("+")[0]
420+ p = $league.find(name)
421+ if p && p.rate != 0
422+ count += 1
423+ sum += p.rate + 100
424+ end
425+ end
426+ history.loss_games(player.player_id).each do |g|
427+ next unless g[:winner]
428+ name = g[:winner].split("+")[0]
429+ p = $league.find(name)
430+ if p && p.rate != 0
431+ count += 1
432+ sum += p.rate - 100
433+ end
434+ end
435+
436+ estimate = (count == 0 ? 2150 : sum/count)
437+ log_message("Floodgate: Estimated rate of %s is %d" % [player.name, estimate])
438+ return estimate
439+ end
440+
441+ def calculate_diff_with_penalty(players, history)
442+ pairs = []
443+ players.each_slice(2) do |pair|
444+ if pair.size == 2
445+ pairs << pair
446+ end
447+ end
448+
449+ ret = 0
450+
451+ # 1. Diff of players rate
452+ pairs.each do |p1,p2|
453+ ret += (get_player_rate(p1,history) - get_player_rate(p2,history)).abs
454+ end
455+
456+ # 2. Penalties
457+ pairs.each do |p1,p2|
458+ # 2.1. same match
459+ if (history &&
460+ (history.last_opponent(p1.player_id) == p2.player_id ||
461+ history.last_opponent(p2.player_id) == p1.player_id))
462+ ret += 400
463+ end
464+
465+ # 2.2 Human vs Human
466+ if p1.is_human? && p2.is_human?
467+ ret += 800
468+ end
469+ end
470+
471+ ret
472+ end
473+
474+ def match(players)
475+ super
476+ if players.size < 3
477+ log_message("Floodgate: players are small enough to skip LeastDiff pairing: %d" % [players.size])
478+ return players
479+ end
480+
481+ # 10 trials
482+ matches = []
483+ scores = []
484+ path = ShogiServer::League::Floodgate.history_file_path(players.first.game_name)
485+ history = ShogiServer::League::Floodgate::History.factory(path)
486+ 10.times do
487+ m = random_match(players)
488+ matches << m
489+ scores << calculate_diff_with_penalty(m, history)
490+ end
491+
492+ # Debug
493+ #scores.each_with_index do |s,i|
494+ # puts
495+ # print s, ": ", matches[i].map{|p| p.name}.join(", "), "\n"
496+ #end
497+
498+ # Select a match of the least score
499+ min_index = 0
500+ min_score = scores.first
501+ scores.each_with_index do |s,i|
502+ if s < min_score
503+ min_index = i
504+ min_score = s
505+ end
506+ end
507+ log_message("Floodgate: the least score %d (%d per player)" % [min_score, min_score/players.size])
508+
509+ players = matches[min_index]
510+ end
511+ end
512+
367513 end # ShogiServer
--- 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
--- 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,246 @@ 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 = [@a,@b,@h]
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+ end
484+
485+ def test_calculate_diff_with_penalty
486+ players = [@a,@b]
487+ assert_equal(@b.rate-@a.rate, @pairing.calculate_diff_with_penalty(players,nil))
488+
489+ dummy = nil
490+ def @history.make_record(game_result)
491+ {:game_id => "wdoor+floodgate-900-0-a-b-1",
492+ :black => "b", :white => "a",
493+ :winner => "a", :loser => "b"}
494+ end
495+ @history.update(dummy)
496+ assert_equal(@b.rate-@a.rate+400, @pairing.calculate_diff_with_penalty(players, @history))
497+ end
498+
499+ def test_calculate_diff_with_penalty2
500+ players = [@a,@b,@g,@h]
501+ assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil))
502+ end
503+
504+ def test_calculate_diff_with_penalty2_1
505+ players = [@a,@b,@g,@h]
506+ assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil))
507+ dummy = nil
508+ def @history.make_record(game_result)
509+ {:game_id => "wdoor+floodgate-900-0-a-b-1",
510+ :black => "b", :white => "a",
511+ :winner => "a", :loser => "b"}
512+ end
513+ @history.update(dummy)
514+ assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players, @history))
515+ end
516+
517+ def test_calculate_diff_with_penalty2_2
518+ players = [@a,@b,@g,@h]
519+ assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil))
520+ dummy = nil
521+ def @history.make_record(game_result)
522+ {:game_id => "wdoor+floodgate-900-0-a-b-1",
523+ :black => "g", :white => "h",
524+ :winner => "h", :loser => "g"}
525+ end
526+ @history.update(dummy)
527+ assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players, @history))
528+ #assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate+400, @pairing.calculate_diff_with_penalty(players, [@b,@a,@h,@g]))
529+ end
530+
531+ def test_calculate_diff_with_penalty2_3
532+ players = [@a,@b,@g,@h]
533+ assert_equal(@b.rate-@a.rate+@h.rate-@g.rate, @pairing.calculate_diff_with_penalty(players,nil))
534+ dummy = nil
535+ def @history.make_record(game_result)
536+ {:game_id => "wdoor+floodgate-900-0-a-b-1",
537+ :black => "g", :white => "h",
538+ :winner => "h", :loser => "g"}
539+ end
540+ @history.update(dummy)
541+ def @history.make_record(game_result)
542+ {:game_id => "wdoor+floodgate-900-0-a-b-1",
543+ :black => "b", :white => "a",
544+ :winner => "a", :loser => "b"}
545+ end
546+ @history.update(dummy)
547+ assert_equal(@b.rate-@a.rate+400+@h.rate-@g.rate+400, @pairing.calculate_diff_with_penalty(players, @history))
548+ end
549+
550+ def test_get_player_rate_0
551+ assert_equal(2150, @pairing.get_player_rate(@x, @history))
552+
553+ dummy = nil
554+ def @history.make_record(game_result)
555+ {:game_id => "wdoor+floodgate-900-0-x-a-1",
556+ :black => "x", :white => "a",
557+ :winner => "x", :loser => "a"}
558+ end
559+ @history.update(dummy)
560+ assert_equal(@a.rate+100, @pairing.get_player_rate(@x, @history))
561+
562+ def @history.make_record(game_result)
563+ {:game_id => "wdoor+floodgate-900-0-x-b-1",
564+ :black => "x", :white => "b",
565+ :winner => "b", :loser => "x"}
566+ end
567+ @history.update(dummy)
568+
569+ assert_equal((@a.rate+100+@b.rate-100)/2, @pairing.get_player_rate(@x, @history))
570+ end
571+end
330572