itb-ranking/main.go
Auke Kok 83bace9b68 Category series.
We use an sqlite GROUP SELECT to automatically rank and return
lists of boxes that have been assigned categories by players. The
whole list of category rankings sits in a single json file.
2019-01-17 22:29:28 -08:00

749 lines
18 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"
"encoding/json"
"fmt"
"github.com/jmoiron/jsonq"
_ "github.com/mattn/go-sqlite3" // MIT licensed.
"log"
"math"
"os"
"sort"
"strings"
)
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
type Series struct {
series_id int
name string
}
var series []Series
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()
// write out series data
rows, err = db.Query("select id, name from series")
if err != nil {
log.Fatal(err)
}
for rows.Next() {
var s Series
err = rows.Scan(&s.series_id, &s.name)
if err != nil {
log.Fatal(err)
}
series = append(series, s)
}
rows.Close()
// make main slice
series_data := make(map[string]interface{})
for i := range series {
c := 0
s := make(map[string]interface{})
d := make(map[string]interface{})
rows, err = db.Query(fmt.Sprintf("select box_id from series_box where series_id=%v order by box_order ASC", series[i].series_id))
for rows.Next() {
var id int
dd := make(map[string]interface{})
err = rows.Scan(&id)
if err != nil {
log.Fatal(err)
}
c += 1
dd["id"] = id
for j := range boxes {
if boxes[j].box_id == id {
dd["builder"] = boxes[j].builder
dd["name"] = boxes[j].name
break
}
}
d[fmt.Sprintf("%v", c)] = dd
}
s["name"] = series[i].name
s["id"] = series[i].series_id
s["boxes"] = d
series_data[fmt.Sprintf("%v", i)] = s
}
sd, err := json.Marshal(series_data)
if err != nil {
log.Fatal(err)
}
// write to disc!
of, err := os.Create("series.json")
if err != nil {
log.Fatal(err)
}
of.Write(sd)
of.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
if count == 0 {
continue
}
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()
//
// CATEGORY RANKING
//
// category icons go from 2..19 atm.
var cat_series = make(map[string]map[string]string) // integer indexed array of arrays
for i := 2; i < 19; i++ {
cat_series[fmt.Sprintf("%v", i)] = make(map[string]string)
cat_rank := 1
// Must have min 3x chosen category, limit to 25 max per cat series
rows, err = db.Query(fmt.Sprintf("SELECT box_id, COUNT(box_id) AS freq FROM points WHERE type='category' AND score=%v GROUP BY box_id HAVING freq > 3 ORDER BY COUNT(box_id) DESC LIMIT 25;", i))
for rows.Next() {
var box int
var count int
err = rows.Scan(&box, &count)
if err != nil {
log.Fatal(err)
}
cat_series[fmt.Sprintf("%v", i)][fmt.Sprintf("%v", cat_rank)] = fmt.Sprintf("%v", box)
cat_rank++
}
}
j, err = json.Marshal(cat_series)
if err != nil {
log.Fatal(err)
}
of, err = os.Create("category_series.json")
if err != nil {
log.Fatal(err)
}
// write out
of.Write(j)
of.Close()
defer db.Close()
}