// // itb-ranking - create rankings from scoring data // // Copyright (c) 2017 - Auke Kok // // 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{}) tutorial_boxes := make(map[int]bool) 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 } if (series[i].series_id == 3) { tutorial_boxes[id] = true } } 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 { // remove tutorial boxes from the box ranking. if (tutorial_boxes[boxes[i].box_id]) { 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 } 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() }