tPrompt players for token to update their score - scoreboard - Interactive scoreboard for CTF-like games
 (HTM) git clone git://git.z3bra.org/scoreboard.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
 (DIR) commit 2dcf67e2ab0f8c6918133d3a5bcb3c1dee86db98
 (DIR) parent e74b632684c58ebb06b40f395409f7759a3ca9fb
 (HTM) Author: Willy Goiffon <contact@z3bra.org>
       Date:   Mon,  5 Dec 2022 18:18:27 +0100
       
       Prompt players for token to update their score
       
       Diffstat:
         M db.go                               |     107 ++++++++++++++-----------------
         M main.go                             |      93 ++++++++++++++++++++++---------
         M mkfile                              |       3 ++-
         M playerbox.go                        |       8 ++++----
         M ui.go                               |      13 ++++++++-----
       
       5 files changed, 130 insertions(+), 94 deletions(-)
       ---
 (DIR) diff --git a/db.go b/db.go
       t@@ -26,104 +26,120 @@ const (
                `
                DB_FILL string = `
                DELETE FROM score;
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('WGS', 'token0', 3,  600,  10);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('DQK', 'token1', 5, 1337,   1);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('VNM', 'token2', 5, 1000, 100);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('PLR', 'token3', 5, 1000, 300);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('WGS', 'token0', 3,  302,   2);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('DQK', 'token1', 5, 1337,  20);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('VNM', 'token2', 5, 1000,   3);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('PLR', 'token3', 5, 1000,   2);
                INSERT INTO score(name,token,flag,score,ts) VALUES ('UKN', 'token4', 2,  200, 200);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('JFK', 'token5', 3,  600, 200);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('NTM', 'token6', 4,  800, 100);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('BOB', 'token7', 1,  200,  25);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('AAA', 'token8', 1,  100,  13);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('JFK', 'token5', 3,  300, 200);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('AAA', 'token6', 1,  130, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('BBB', 'token7', 4,  400, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('CCC', 'token8', 4,  400, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('DDD', 'token9', 4,  407, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('EEE', 'tokenA', 4,  405, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('FFF', 'tokenB', 4,  401, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('GGG', 'tokenC', 4,  406, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('HHH', 'tokenD', 4,  408, 1670260445);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('III', 'tokenE', 4,  409, 1670260435);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('JJJ', 'tokenF', 4,  402, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('KKK', 'tokenG', 4,  400, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('LLL', 'tokenH', 4,  404, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('MMM', 'tokenI', 4,  403, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('NNN', 'tokenJ', 4,  400, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('OOO', 'tokenK', 4,  400, 1670260535);
       +        INSERT INTO score(name,token,flag,score,ts) VALUES ('PPP', 'tokenL', 4,  400, 1670260535);
                `
        )
        
        
       -func (a *Application) db_init(file string) error {
       +func db_init(file string) (*sql.DB, error) {
                var err error
       +        var db *sql.DB
        
                // open database
       -        a.db, err = sql.Open("sqlite", file)
       +        db, err = sql.Open("sqlite", file)
                if err != nil {
       -                return err
       +                return nil, err
                }
        
                // create schema if needed
       -        _, err = a.db.Exec(DB_CREATE)
       +        _, err = db.Exec(DB_CREATE)
                if err != nil {
       -                return err
       +                return nil, err
                }
                ///*
       -        _, err = a.db.Exec(DB_FILL)
       +        _, err = db.Exec(DB_FILL)
                if err != nil {
                        panic(err)
                }
                //*/
       -        return nil
       +        return db, nil
        }
        
       -func (a *Application) db_count() int {
       +func db_count(db *sql.DB) int {
                var count int
                query := `SELECT count(id) FROM score;`
       -        row := a.db.QueryRow(query)
       +        row := db.QueryRow(query)
                row.Scan(&count)
                return count
        }
        
       -func (a *Application) db_count_flag(flag int) int {
       +func db_score_count(db *sql.DB, score, ts int) int {
                var count int
                query := `SELECT
                  count(id)
                  FROM score
                  WHERE
       -            flags >= ?
       +            score >= ? AND
       +            ts => ?
                ;`
        
       -        row := a.db.QueryRow(query, flag)
       +        row := db.QueryRow(query, score, ts)
                row.Scan(&count)
                return count
        }
        
       -func (a *Application) db_checkname(nick string) bool {
       +func db_flag_count(db *sql.DB, flag int) int {
                var count int
                query := `SELECT
                  count(id)
                  FROM score
                  WHERE
       -            name = ?
       +            flag >= ?
                ;`
        
       -        row := a.db.QueryRow(query, nick)
       +        row := db.QueryRow(query, flag)
                row.Scan(&count)
       -        return (count > 0)
       +        return count
        }
        
       -func (a *Application) db_rank(score int, ts int64) int {
       -        var rank int
       +func db_id(db *sql.DB, nick string) bool {
       +        var count int
                query := `SELECT
                  count(id)
                  FROM score
                  WHERE
       -            score >= ?
       +            name = ?
                ;`
        
       -        row := a.db.QueryRow(query, score)
       -        row.Scan(&rank)
       -        return rank + 1
       +        row := db.QueryRow(query, nick)
       +        row.Scan(&count)
       +        return (count > 0)
        }
        
       -func (a *Application) db_ranked(offset, limit int) ([]Player, error) {
       +func db_ranked_players(db *sql.DB, offset, limit int) ([]Player, error) {
                query := `SELECT
                  id,name,token,flag,score
                  FROM score
                  ORDER BY
                    score DESC,
       +            flag DESC,
                    ts ASC
                  LIMIT ?
                  OFFSET ?
                ;`
        
       -        rows, err := a.db.Query(query, limit, offset)
       +        rows, err := db.Query(query, limit, offset)
                if err != nil {
                        return nil, err
                }
       t@@ -131,7 +147,7 @@ func (a *Application) db_ranked(offset, limit int) ([]Player, error) {
                players := make([]Player, 0)
                for rows.Next() {
                        var p Player
       -                err := rows.Scan(&p.id, &p.name, &p.token, &p.flags, &p.score)
       +                err := rows.Scan(&p.id, &p.name, &p.token, &p.flag, &p.score)
                        if err != nil {
                                return nil, err
                        }
       t@@ -140,28 +156,3 @@ func (a *Application) db_ranked(offset, limit int) ([]Player, error) {
        
                return players, nil
        }
       -
       -func (a *Application) db_player(id int) *Player {
       -        var p Player
       -        query := `SELECT id,name,token,flag,score,ts FROM score WHERE id = ?`
       -
       -        row := a.db.QueryRow(query, p.id)
       -        err := row.Scan(&p.id, &p.name, &p.token, &p.flags, &p.score)
       -        if err == sql.ErrNoRows {
       -                return nil
       -        }
       -
       -        return &p
       -}
       -
       -func (a *Application) db_save(p Player) int64 {
       -        query := `INSERT INTO score(name,token,flag,score,ts) VALUES(?,?,?,?,?);`
       -        r, err := a.db.Exec(query, p.name, p.token, p.flags, p.score, p.ts)
       -        if err != nil {
       -                panic(err)
       -        }
       -
       -        id, _ := r.LastInsertId()
       -
       -        return id
       -}
 (DIR) diff --git a/main.go b/main.go
       t@@ -34,16 +34,8 @@ const (
                DB string = "leaderboard.db"
        )
        
       -type Player struct {
       -        id int
       -        name string
       -        token string
       -        flags int
       -        score int
       -        ts int64
       -}
       -
        type Application struct {
       +        flag int
                db *sql.DB
                app *tview.Application
                pages *tview.Pages
       t@@ -54,11 +46,15 @@ type Application struct {
        
        var flag_sha256 = [...]string {
                "A",
       +        "B",
       +        "C",
       +        "D",
       +        "E"}
                //"A6322C6522FB49959242670BC62E85A29ED7EB8CFCD87BE91521F0CA5EEBD198", // cookie.png
       -        "AFE502310D1EE1494770A46DEEED25CABB4B0CA70EFD0571C249C6BAA3728B46", // onion.png + zip
       -        "AC899E9F52E7194933E64D26800A609A83FF01826347FEA5ECDB2E420CAF1F43", // cream.png
       -        "8CB250A66D4301244699186CA723E11AFA806C64466789AB14B7027A2F928BF8", // egg.png
       -        "F6A4071C9C0DDCCB53FC1CDCA38E32CF10551F75AA48996A9341842A7EF2591B"} // salt.png
       +        //"AFE502310D1EE1494770A46DEEED25CABB4B0CA70EFD0571C249C6BAA3728B46", // onion.png + zip
       +        //"AC899E9F52E7194933E64D26800A609A83FF01826347FEA5ECDB2E420CAF1F43", // cream.png
       +        //"8CB250A66D4301244699186CA723E11AFA806C64466789AB14B7027A2F928BF8", // egg.png
       +        //"F6A4071C9C0DDCCB53FC1CDCA38E32CF10551F75AA48996A9341842A7EF2591B"} // salt.png
        
        
        var cyboard = Application{
       t@@ -88,18 +84,57 @@ func pageFlag() tview.Primitive {
                        return matched
                })
                input.SetDoneFunc(func(key tcell.Key) {
       -                        switch flagid(input.GetText()) {
       -                        case 0,1,2,3,4:
       +                        if cyboard.flag == 1 {
                                        cyboard.pages.SwitchToPage("score")
       -                        default:
       -                                cyboard.app.Stop()
       -                                fmt.Println("Incorrect flag")
                                }
                        })
        
                return center(40, 1, input)
        }
        
       +func pageToken() tview.Primitive {
       +        input := tview.NewInputField().
       +                SetLabel("TOKEN ").
       +                SetPlaceholder("").
       +                SetFieldStyle(tcell.StyleDefault.Reverse(true)).
       +                SetFieldWidth(28)
       +
       +        input.SetAcceptanceFunc(func(text string, ch rune) bool {
       +                matched, err := regexp.Match(`^[a-zA-Z0-9]+$`, []byte(text))
       +                if err != nil {
       +                        panic(err)
       +                }
       +                return matched
       +        })
       +        input.SetDoneFunc(func(key tcell.Key) {
       +                        _, err := cyboard.player.FromToken(input.GetText())
       +                        if err != nil {
       +                                cyboard.Popup("Error", "Invalid Token")
       +                                return
       +                        }
       +
       +                        if cyboard.player.flag >= cyboard.flag {
       +                                cyboard.Popup("Error", "Flag already submitted")
       +                                return
       +                        }
       +
       +                        cyboard.player.ts = time.Now().Unix()
       +                        cyboard.player.flag = cyboard.flag
       +                        cyboard.player.score += 100
       +
       +                        n := cyboard.player.FlagRank()
       +                        if n < 10 {
       +                                cyboard.player.score += 10 - n
       +                        }
       +
       +                        cyboard.player.Update()
       +                        cyboard.HighlightBoard(cyboard.player.ScoreRank())
       +                        cyboard.pages.SwitchToPage("board")
       +                })
       +
       +        return center(40, 1, input)
       +}
       +
        func pageBoard() tview.Primitive {
                cyboard.SetupFrame()
                cyboard.DrawBoard()
       t@@ -108,6 +143,7 @@ func pageBoard() tview.Primitive {
        }
        
        func main() {
       +        var err error
                cmd := "board"
        
                flag.Parse()
       t@@ -131,12 +167,17 @@ func main() {
                tview.Styles.GraphicsColor            = tcell.ColorDefault
                tview.Styles.PrimaryTextColor         = tcell.ColorDefault
        
       -        cyboard.db_init(DB)
       +        cyboard.db, err = db_init(DB)
       +        if err != nil {
       +                panic(err)
       +        }
       +        cyboard.player.db = cyboard.db
                defer cyboard.db.Close()
        
                cyboard.pages.SetBackgroundColor(tcell.ColorDefault)
        
                cyboard.pages.AddPage("flag",   pageFlag(), true, false)
       +        cyboard.pages.AddPage("token",  pageToken(), true, false)
                cyboard.pages.AddPage("board",  pageBoard(), true, false)
        
                switch cmd {
       t@@ -144,24 +185,24 @@ func main() {
                        if (len(args) < 2) {
                                usage()
                        }
       -                switch flagid(args[1]) {
       -                case 0:
       -                        cyboard.player.flags = 1
       +                switch cyboard.flag = flagid(args[1]) + 1; cyboard.flag {
       +                case 1:
       +                        cyboard.player.flag = cyboard.flag
                                cyboard.player.score = 100
                                cyboard.player.ts = time.Now().Unix()
        
                                /* Bonus points for the first 10 players to get first flag */
       -                        n := cyboard.db_count_flag(1)
       +                        n := cyboard.player.FlagRank()
                                if n < 10 {
                                        cyboard.player.score += 10 - n
                                }
        
       -                        rank := cyboard.db_rank(cyboard.player.score, cyboard.player.ts)
       +                        rank := cyboard.player.ScoreRank() + 1
        
                                cyboard.NewPlayer(rank)
                                cyboard.pages.SwitchToPage("board")
       -                //case 1,2,3,4:
       -                //        pages.ShowPage("token")
       +                case 2,3,4,5:
       +                        cyboard.pages.ShowPage("token")
                        default:
                                fmt.Println("Incorrect flag")
                        }
 (DIR) diff --git a/mkfile b/mkfile
       t@@ -1,4 +1,5 @@
        GO = go
       +SRC = `{find . -name '*.go'}
        
       -scoreboard: main.go db.go playerbox.go util.go ui.go
       +scoreboard: ${SRC}
                ${GO} build
 (DIR) diff --git a/playerbox.go b/playerbox.go
       t@@ -46,7 +46,7 @@ func manipulatebox(event *tcell.EventKey) *tcell.EventKey {
                                }
                        case tcell.KeyUp:
                                playerbox.char[playerbox.cur]++
       -                        if playerbox.char[playerbox.cur] > len(charlist) {
       +                        if playerbox.char[playerbox.cur] >= len(charlist) {
                                        playerbox.char[playerbox.cur] = 0
                                }
                        case tcell.KeyDown:
       t@@ -70,8 +70,8 @@ func PlayerBoxName(p Player) *tview.TextView {
                        SetDoneFunc(func(key tcell.Key) {
                                if key == tcell.KeyEnter {
                                        p.name = fmt.Sprintf("%3s", playerbox.name)
       -                                if cyboard.db_checkname(p.name) == false {
       -                                        cyboard.db_save(p)
       +                                if ! p.Exists() {
       +                                        p.Register()
                                                cyboard.DrawBoard()
                                        } else {
                                                cyboard.Popup("Error", "Name already registered\nPlease pick another one")
       t@@ -109,7 +109,7 @@ func PlayerBoxGrid(p Player, rank int) *tview.Grid {
                        SetRows(1).
                        AddItem(gridcell(rankstr), 0, 0, 1, 1, 0, 0, false).
                        AddItem(box, 0, 1, 1, 1, 0, 0, true).
       -                AddItem(gridcell(flag2str(p.flags)), 0, 2, 1, 1, 0, 0, false).
       +                AddItem(gridcell(flag2str(p.flag)), 0, 2, 1, 1, 0, 0, false).
                        AddItem(playerbox.score, 0, 3, 1, 1, 0, 0, false)
        
                return grid
 (DIR) diff --git a/ui.go b/ui.go
       t@@ -17,10 +17,7 @@ func BoardHeader() *tview.TextView {
        
        func RankTable(offset, limit, rank int, fill bool) *tview.Table {
                t := tview.NewTable()
       -        if limit < 0 {
       -                limit = cyboard.db_count()
       -        }
       -        players, err := cyboard.db_ranked(offset, limit)
       +        players, err := db_ranked_players(cyboard.db, offset, limit)
                if err != nil {
                        panic(err)
                }
       t@@ -31,7 +28,7 @@ func RankTable(offset, limit, rank int, fill bool) *tview.Table {
                for i := 0; i < len(players); i++ {
                        p := players[i]
                        rankstr := fmt.Sprintf("%4s", humanize.Ordinal(rank + i + 1))
       -                flagstr := flag2str(p.flags)
       +                flagstr := flag2str(p.flag)
                        scorestr := fmt.Sprintf("%4d", p.score)
                        t.SetCell(i, 0, newcell(rankstr))
                        t.SetCell(i, 1, newcell(p.name))
       t@@ -72,6 +69,12 @@ func (a *Application) DrawBoard() {
                        AddItem(RankTable(0, -1, 0, true), BOARD_HEIGHT, 1, true)
        }
        
       +func (a *Application) HighlightBoard(line int) {
       +        cyboard.board.Clear().
       +                SetDirection(tview.FlexRow).
       +                AddItem(RankTable(0, -1, 0, true).Select(line - 1, 0), BOARD_HEIGHT, 1, true)
       +}
       +
        func (a *Application) NewPlayer(rank int) {
                t1 := RankTable(0, BOARD_HEIGHT, 0, false).SetSelectable(false, false)
                t2 := RankTable(rank - 1, BOARD_HEIGHT, rank, true).SetSelectable(false, false)