Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}