itb-ranking/main.go

818 lines
20 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", fmt.Sprintf("file:%v?mode=rwc&_query_only=true", 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;")
if err != nil {
log.Fatal(err)
}
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 })
var player_raw = make(map[string]string)
for i := 0; i < len(players); 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_raw[fmt.Sprintf("%v", i+1)] = fmt.Sprintf("%v (%v): %v - %v %v %v %v",
players[i].player_id,
name,
players[i].combined,
players[i].completed_rank,
players[i].time_rank,
players[i].damage_rank,
players[i].deaths_rank)
}
j, err := json.Marshal(player_raw)
if err != nil {
log.Fatal(err)
}
// write to disc!
of, err = os.Create("raw_players.json")
if err != nil {
log.Fatal(err)
}
of.Write(j)
of.Close()
// 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 })
var boxes_raw = make(map[string]string)
c := 0
for i := range boxes {
c = c + 1
boxes_raw[fmt.Sprintf("%v", c)] = fmt.Sprintf("%v: %v - %v %v %v %v %v %v",
boxes[i].box_id,
boxes[i].combined,
boxes[i].avg_damage_rank,
boxes[i].avg_deaths_rank,
boxes[i].avg_time_rank,
boxes[i].avg_rating_rank,
boxes[i].completed_players_rank,
boxes[i].completed_times_rank)
}
j, err = json.Marshal(boxes_raw)
if err != nil {
log.Fatal(err)
}
of, err = os.Create("raw_boxes.json")
if err != nil {
log.Fatal(err)
}
of.Write(j)
of.Close()
c = 0
var boxes_topranks = make(map[string]string)
for i := range boxes {
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
}
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_raw = make(map[string]string)
for i := range builders {
builder_raw[fmt.Sprintf("%v", i+1)] = fmt.Sprintf("%v: %v - %v %v",
builders[i].name,
builders[i].combined,
builders[i].box_rank_avg_rank,
builders[i].box_count_rank)
}
j, err = json.Marshal(builder_raw)
if err != nil {
log.Fatal(err)
}
// write to disc!
of, err = os.Create("raw_builders.json")
if err != nil {
log.Fatal(err)
}
of.Write(j)
of.Close()
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()
}