itb-ranking/main.go
Auke Kok bab1a538eb Builder ranking.
Combines count + box ranking (1:1).

This balances quantity and quality, somewhat.
2017-11-05 22:55:17 -08:00

642 lines
15 KiB
Go

//
// itb-ranking - create rankings from scoring data
//
// Copyright (c) 2017 - Auke Kok <sofar@foo-projects.org>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // MIT licensed.
"github.com/jmoiron/jsonq"
"strings"
"encoding/json"
"fmt"
"log"
"os"
"sort"
"math"
)
type Score struct {
player_id int
box_id int
stype string
score float64
}
var scores []Score
type Bmeta struct {
box_id int
btype int
meta string
builder string
name string
// used for player rankings
worst_time float64
worst_damage float64
worst_deaths float64
// box ranking elements
completed_players int
completed_players_rank float64
completed_times int
completed_times_rank float64
avg_rating float64
avg_rating_rank float64
avg_time float64
avg_time_rank float64
avg_damage float64
avg_damage_rank float64
avg_deaths float64
avg_deaths_rank float64
combined float64
}
var boxes []Bmeta
type Player struct {
player_id int
completed_boxes map[int]bool
completed int
completed_rank float64
time float64
time_rank float64
damage float64
damage_rank float64
deaths float64
deaths_rank float64
combined float64
}
var players []Player
type Builder struct {
player_id int
name string
combined float64
box_count int
box_count_rank float64
box_rank_avg float64
box_rank_avg_rank float64
}
var builders []Builder
func main() {
if len(os.Args) < 1 {
log.Fatal("Not enough arguments: sqlite_file")
}
f := os.Args[1]
db, err := sql.Open("sqlite3", f)
if err != nil {
log.Fatal(err)
}
//
// sqlite data fetching - for all data needed
//
// fetch Box meta
rows, err := db.Query("select id, type, meta from box_meta")
for rows.Next() {
var b Bmeta
err = rows.Scan(&b.box_id, &b.btype, &b.meta)
if b.btype == 0 {
data := map[string]interface{}{}
dec := json.NewDecoder(strings.NewReader(b.meta))
dec.Decode(&data)
jq := jsonq.NewQuery(data)
status, _ := jq.Int("status")
b.builder, _ = jq.String("builder")
b.name, _ = jq.String("box_name")
if status == 2 {
boxes = append(boxes, b)
}
}
if err != nil {
log.Fatal(err)
}
}
// fetch player data
rows, err = db.Query("select player_id, box_id, type, score from points")
if err != nil {
log.Fatal(err)
}
for rows.Next() {
var s Score
var player_id int
var box_id int
var stype string
var score float64
err = rows.Scan(&player_id, &box_id, &stype, &score)
if err != nil {
log.Fatal(err)
}
s.player_id = player_id
s.box_id = box_id
s.stype = stype
s.score = score
found := false
for i := range boxes {
if boxes[i].box_id == s.box_id {
found = true
break
}
}
if found {
scores = append(scores, s)
}
}
rows.Close()
//
// PLAYER RANKING
//
// player ranking - create base player list first, and add boxes completed
for _, p := range scores {
if p.stype == "time" {
// prune admins!
if (p.player_id == 1596) || (p.player_id == 2981) {
continue
}
found := false
for i := range players {
if players[i].player_id == p.player_id {
found = true
if ! players[i].completed_boxes[p.box_id] {
players[i].completed_boxes[p.box_id] = true
players[i].completed += 1
}
break
}
}
if ! found {
var n Player
n.player_id = p.player_id
n.completed_boxes = make(map[int]bool)
n.completed_boxes[p.box_id] = true
n.completed = 1
players = append(players, n)
}
}
}
// rank boxes completed
sort.Slice(players, func (a, b int) bool { return players[a].completed > players[b].completed })
step := 1.0 / float64(len(players))
lastrank := 1.0
last := 0
for i := range players {
c := players[i].completed
if c == last {
players[i].completed_rank = lastrank
} else {
last = c
lastrank = 1.0 - step * float64(i)
players[i].completed_rank = lastrank
}
}
// time/deaths/damage ranking
// for this, we need to know the WORST scores for each box, since that
// will be the ranking value for players that have not completed that box
for i := range boxes {
for j := range scores {
if scores[j].box_id == boxes[i].box_id {
if scores[j].stype == "time" {
if scores[j].score > boxes[i].worst_time {
boxes[i].worst_time = scores[j].score
}
}
if scores[j].stype == "damage" {
if scores[j].score > boxes[i].worst_damage {
boxes[i].worst_damage = scores[j].score
}
}
if scores[j].stype == "deaths" {
if scores[j].score > boxes[i].worst_deaths {
boxes[i].worst_deaths = scores[j].score
}
}
}
}
}
for i := range players {
for j := range boxes {
time := boxes[j].worst_time
damage := boxes[j].worst_damage
deaths := boxes[j].worst_deaths
for k := range scores {
if scores[k].box_id == boxes[j].box_id &&
scores[k].player_id == players[i].player_id {
if scores[k].stype == "time" {
time = math.Min(time, scores[k].score)
}
if scores[k].stype == "damage" {
damage = math.Min(damage, scores[k].score)
}
if scores[k].stype == "deaths" {
deaths = math.Min(deaths, scores[k].score)
}
}
}
players[i].time += time
players[i].damage += damage
players[i].deaths += deaths
}
}
// rank by time
sort.Slice(players, func (a, b int) bool { return players[a].time < players[b].time })
step = 1.0 / float64(len(players))
lastrank = 1.0
lastf := .0
for i := range players {
c := players[i].time
if c == lastf {
players[i].time_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
players[i].time_rank = lastrank
}
}
// rank by damage
sort.Slice(players, func (a, b int) bool { return players[a].damage < players[b].damage })
step = 1.0 / float64(len(players))
lastrank = 1.0
lastf = .0
for i := range players {
c := players[i].damage
if c == lastf {
players[i].damage_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
players[i].damage_rank = lastrank
}
}
// rank by deaths
sort.Slice(players, func (a, b int) bool { return players[a].deaths < players[b].deaths })
step = 1.0 / float64(len(players))
lastrank = 1.0
lastf = .0
for i := range players {
c := players[i].deaths
if c == lastf {
players[i].deaths_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
players[i].deaths_rank = lastrank
}
}
// rank all together now
for i := range players {
players[i].combined =
players[i].completed_rank +
players[i].time_rank +
players[i].damage_rank +
players[i].deaths_rank
}
// sort and we're done, no need to normalize ranking values
sort.Slice(players, func (a, b int) bool { return players[a].combined > players[b].combined })
// write out ranking JSON
var player_topranks = make(map[string]string)
var ranklen = 30
if len(players) < ranklen {
ranklen = len(players)
}
for i := 0; i < ranklen; i++ {
var name string
err = db.QueryRow(fmt.Sprintf("SELECT name FROM player WHERE id = \"%v\"", players[i].player_id)).Scan(&name)
if err != nil {
log.Fatal(err)
}
player_topranks[fmt.Sprintf("%v", i + 1)] = name
}
j, err := json.Marshal(player_topranks)
if err != nil {
log.Fatal(err)
}
// write to disc!
of, err := os.Create("top_players.json")
if err != nil {
log.Fatal(err)
}
of.Write(j)
of.Close();
//
// BOX RANKING
//
// completed_times, completed_players
for i := range boxes {
// make sure to filter out builder runs
var builder int
err = db.QueryRow(fmt.Sprintf("SELECT id FROM player WHERE name = \"%v\"", boxes[i].builder)).Scan(&builder)
if err != nil {
log.Fatal(err)
}
count := 0
err = db.QueryRow(fmt.Sprintf("SELECT COUNT(player_id) FROM POINTS WHERE box_id = \"%v\" AND type = \"time\" AND player_id <> \"%v\"", boxes[i].box_id, builder)).Scan(&count)
if (err != nil) && (err != sql.ErrNoRows) {
log.Fatal(err)
}
boxes[i].completed_times = count
pcount := 0
err = db.QueryRow(fmt.Sprintf("SELECT COUNT(DISTINCT player_id) FROM POINTS WHERE box_id = \"%v\" AND type = \"time\" AND player_id <> \"%v\"", boxes[i].box_id, builder)).Scan(&pcount)
if (err != nil) && (err != sql.ErrNoRows) {
log.Fatal(err)
}
boxes[i].completed_players = pcount
rating := .0
err = db.QueryRow(fmt.Sprintf("SELECT AVG(score) FROM points WHERE type='rating' AND box_id = \"%v\" AND player_id <> \"%v\"", boxes[i].box_id, builder)).Scan(&rating)
//if (err != nil) && (err != sql.ErrNoRows) {
// log.Fatal(err)
//}
boxes[i].avg_rating = rating
time := .0
err = db.QueryRow(fmt.Sprintf("SELECT AVG(score) FROM points WHERE type='time' AND box_id = \"%v\" AND player_id <> \"%v\"", boxes[i].box_id, builder)).Scan(&time)
if (err != nil) && (err != sql.ErrNoRows) {
log.Fatal(err)
}
boxes[i].avg_time = time
deaths := .0
err = db.QueryRow(fmt.Sprintf("SELECT AVG(score) FROM points WHERE type='deaths' AND box_id = \"%v\" AND player_id <> \"%v\"", boxes[i].box_id, builder)).Scan(&deaths)
if (err != nil) && (err != sql.ErrNoRows) {
log.Fatal(err)
}
boxes[i].avg_deaths = deaths
damage := .0
err = db.QueryRow(fmt.Sprintf("SELECT AVG(score) FROM points WHERE type='damage' AND box_id = \"%v\" AND player_id <> \"%v\"", boxes[i].box_id, builder)).Scan(&damage)
if (err != nil) && (err != sql.ErrNoRows) {
log.Fatal(err)
}
boxes[i].avg_damage = damage
}
// rank box data
sort.Slice(boxes, func (a, b int) bool { return boxes[a].completed_times > boxes[b].completed_times })
step = 1.0 / float64(len(boxes))
lastrank = 1.0
last = 0
for i := range boxes {
c := boxes[i].completed_times
if c == last {
boxes[i].completed_times_rank = lastrank
} else {
last = c
lastrank = 1.0 - step * float64(i)
boxes[i].completed_times_rank = lastrank
}
}
sort.Slice(boxes, func (a, b int) bool { return boxes[a].completed_players > boxes[b].completed_players })
step = 1.0 / float64(len(boxes))
lastrank = 1.0
last = 0
for i := range boxes {
c := boxes[i].completed_players
if c == last {
boxes[i].completed_players_rank = lastrank
} else {
last = c
lastrank = 1.0 - step * float64(i)
boxes[i].completed_players_rank = lastrank
}
}
sort.Slice(boxes, func (a, b int) bool { return boxes[a].avg_rating > boxes[b].avg_rating })
step = 1.0 / float64(len(boxes))
lastrank = 1.0
lastf = .0
for i := range boxes {
c := boxes[i].avg_rating
if c == lastf {
boxes[i].avg_rating_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
boxes[i].avg_rating_rank = lastrank
}
}
sort.Slice(boxes, func (a, b int) bool { return boxes[a].avg_time > boxes[b].avg_time })
step = 1.0 / float64(len(boxes))
lastrank = 1.0
lastf = .0
for i := range boxes {
c := boxes[i].avg_time
if c == lastf {
boxes[i].avg_time_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
boxes[i].avg_time_rank = lastrank
}
}
sort.Slice(boxes, func (a, b int) bool { return boxes[a].avg_deaths > boxes[b].avg_deaths })
step = 1.0 / float64(len(boxes))
lastrank = 1.0
lastf = .0
for i := range boxes {
c := boxes[i].avg_deaths
if c == lastf {
boxes[i].avg_deaths_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
boxes[i].avg_deaths_rank = lastrank
}
}
sort.Slice(boxes, func (a, b int) bool { return boxes[a].avg_damage > boxes[b].avg_damage })
step = 1.0 / float64(len(boxes))
lastrank = 1.0
lastf = .0
for i := range boxes {
c := boxes[i].avg_damage
if c == lastf {
boxes[i].avg_damage_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
boxes[i].avg_damage_rank = lastrank
}
}
// glob all the rankings
for i := range boxes {
boxes[i].combined =
(.5 * boxes[i].avg_damage_rank) +
(.5 * boxes[i].avg_deaths_rank) +
boxes[i].avg_time_rank +
boxes[i].avg_rating_rank +
(.5 * boxes[i].completed_players_rank) +
(.5 * boxes[i].completed_times_rank)
}
// sort and output
sort.Slice(boxes, func (a, b int) bool { return boxes[a].combined > boxes[b].combined })
c := 0
var boxes_topranks = make(map[string]string)
for i := range boxes {
if (boxes[i].builder == "sofar") || (boxes[i].builder == "nore") {
continue
}
c = c + 1
boxes_topranks[fmt.Sprintf("%v", c)] = fmt.Sprintf("%v: \"%v\" by %v", boxes[i].box_id, boxes[i].name, boxes[i].builder)
if (c > ranklen) {
break
}
}
j, err = json.Marshal(boxes_topranks)
if err != nil {
log.Fatal(err)
}
// write to disc!
of, err = os.Create("top_boxes.json")
if err != nil {
log.Fatal(err)
}
of.Write(j)
of.Close();
//
// BUILDER RANKING
//
for i := range boxes {
found := false
for b := range builders {
if builders[b].name == boxes[i].builder {
found = true
s := builders[b].box_rank_avg * float64(builders[b].box_count) + boxes[i].combined
builders[b].box_count += 1
builders[b].box_rank_avg = s / float64(builders[b].box_count)
break
}
}
if found {
continue
}
if (boxes[i].builder == "sofar") || (boxes[i].builder == "nore") {
continue
}
var n Builder
n.name = boxes[i].builder
n.box_count = 1
n.box_rank_avg = boxes[i].combined
builders = append(builders, n)
}
// rank box count
sort.Slice(builders, func (a, b int) bool { return builders[a].box_count > builders[b].box_count })
step = 1.0 / float64(len(builders))
lastrank = 1.0
last = 0
for i := range builders {
c := builders[i].box_count
if c == last {
builders[i].box_count_rank = lastrank
} else {
last = c
lastrank = 1.0 - step * float64(i)
builders[i].box_count_rank = lastrank
}
}
// rank boxes scoring
sort.Slice(builders, func (a, b int) bool { return builders[a].box_rank_avg > builders[b].box_rank_avg })
step = 1.0 / float64(len(builders))
lastrank = 1.0
lastf = .0
for i := range builders {
c := builders[i].box_rank_avg
if c == lastf {
builders[i].box_rank_avg_rank = lastrank
} else {
lastf = c
lastrank = 1.0 - step * float64(i)
builders[i].box_rank_avg_rank = lastrank
}
}
// combine
for i := range builders {
builders[i].combined =
builders[i].box_rank_avg_rank +
builders[i].box_count_rank
}
sort.Slice(builders, func (a, b int) bool { return builders[a].combined > builders[b].combined })
var builder_topranks = make(map[string]string)
for i := range builders {
if i > 30 {
break
}
builder_topranks[fmt.Sprintf("%v", i + 1)] = fmt.Sprintf("%v", builders[i].name)
}
j, err = json.Marshal(builder_topranks)
if err != nil {
log.Fatal(err)
}
// write to disc!
of, err = os.Create("top_builders.json")
if err != nil {
log.Fatal(err)
}
of.Write(j)
of.Close();
defer db.Close()
}