rating.gno
4.19 Kb · 147 lines
1package arena
2
3import "gno.land/p/g188vafzutzvlfw6wt7u3s62xtuvm0epmu8ufmfz/gnomepit/engine"
4
5const (
6 startRating = 1000
7 maxRating = 0xFFFF // a ladder key holds the rating in 2 bytes
8 playerK = 32
9 clanK = 16
10)
11
12// expectedTable is the expected score, in thousandths, of a side whose opponent is rated
13// 0, 25, …, 400 points above it: 1 / (1 + 10^(d / 400)), rounded.
14var expectedTable = [17]int{500, 464, 429, 394, 360, 327, 297, 267, 240, 215, 192, 170, 151, 133, 118, 104, 91}
15
16// expected is the expected score, in thousandths, of a side rated a against one rated b.
17// The gap is capped at 400 points and rounded to the nearest 25.
18func expected(a, b int) int {
19 d := b - a
20 if d < 0 {
21 return 1000 - expected(b, a)
22 }
23 if d > 400 {
24 d = 400
25 }
26 return expectedTable[(d+12)/25]
27}
28
29// eloChange is the change, rounded to the nearest point, of a side rated a that scored
30// score thousandths (1000 a win, 500 a draw, 0 a loss) against one rated b.
31func eloChange(a, b, score, k int) int {
32 x := k * (score - expected(a, b))
33 if x < 0 {
34 return -((-x + 500) / 1000)
35 }
36 return (x + 500) / 1000
37}
38
39// applyChange gives a the change delta and b its negative, each kept within 0 to maxRating.
40func applyChange(a, b, delta int) (int, int) {
41 return clampRating(a + delta), clampRating(b - delta)
42}
43
44func clampRating(r int) int {
45 if r < 0 {
46 return 0
47 }
48 if r > maxRating {
49 return maxRating
50 }
51 return r
52}
53
54// rate counts match m, which has just ended, in both players' standings and on the
55// ladder, and in the clan records when two different clans met. A match its challenger
56// accepted is not rated.
57func rate(rules *engine.Ruleset, m record) {
58 if m.challenger == m.acceptor {
59 return
60 }
61 score := scoreOf(m.result)
62 a, b := ratedStanding(rules, m.challenger), ratedStanding(rules, m.acceptor)
63 a.rating, b.rating = applyChange(a.rating, b.rating, eloChange(a.rating, b.rating, score, playerK))
64 a.count(score, m.clans[0])
65 b.count(1000-score, m.clans[1])
66 ladder, _ = ladder.Set(ladderKey(a.rating, m.challenger), nil)
67 ladder, _ = ladder.Set(ladderKey(b.rating, m.acceptor), nil)
68 saveStanding(m.challenger, a)
69 saveStanding(m.acceptor, b)
70
71 if m.clans[0] == m.clans[1] {
72 return
73 }
74 c := decodeClans(clanRecords, len(rules.Clans))
75 ca, cb := &c[m.clans[0]-1], &c[m.clans[1]-1]
76 ca.rating, cb.rating = applyChange(ca.rating, cb.rating, eloChange(ca.rating, cb.rating, score, clanK))
77 tally(score, &ca.wins, &ca.losses, &ca.draws)
78 tally(1000-score, &cb.wins, &cb.losses, &cb.draws)
79 clanRecords = encodeClans(c)
80}
81
82// ratedStanding returns a's standing, created at startRating for a first rated match, with
83// a count for every clan of rules, and takes its key off the ladder until it is counted.
84func ratedStanding(rules *engine.Ruleset, a address) *standing {
85 st := loadStanding(a)
86 if st == nil {
87 st = &standing{rating: startRating}
88 } else {
89 ladder, _, _, _ = ladder.Remove(ladderKey(st.rating, a))
90 }
91 for len(st.perClan) < len(rules.Clans) {
92 st.perClan = append(st.perClan, 0)
93 }
94 return st
95}
96
97// scoreOf is the challenger's score, in thousandths, in a match that ended with result.
98func scoreOf(result string) int {
99 switch result {
100 case "a":
101 return 1000
102 case "b":
103 return 0
104 case "draw":
105 return 500
106 }
107 panic("unknown result")
108}
109
110// count adds a match scored score thousandths and played as clan number clan.
111func (s *standing) count(score, clan int) {
112 tally(score, &s.wins, &s.losses, &s.draws)
113 s.lastClan = clan
114 s.perClan[clan-1]++
115}
116
117func tally(score int, wins, losses, draws *int) {
118 switch score {
119 case 1000:
120 *wins++
121 case 0:
122 *losses++
123 default:
124 *draws++
125 }
126}
127
128// mainClan is the clan number s has finished the most rated matches with, 0 for none. A
129// tie goes to the last match's clan when it is one of the tied, else to ruleset order.
130func (s standing) mainClan() int {
131 best := 0
132 for i, n := range s.perClan {
133 if n > 0 && (best == 0 || n > s.perClan[best-1]) {
134 best = i + 1
135 }
136 }
137 if best != 0 && s.lastClan >= 1 && s.lastClan <= len(s.perClan) && s.perClan[s.lastClan-1] == s.perClan[best-1] {
138 return s.lastClan
139 }
140 return best
141}
142
143// rankOf is the rank a rating holds: 1 plus the number of players rated above it.
144func rankOf(rating int) int {
145 i, _, _ := ladder.Get(ladderKey(rating, ""))
146 return i + 1
147}