tMake board editable in-place - scoreboard - Interactive scoreboard for CTF-like games
 (HTM) git clone git://git.z3bra.org/scoreboard.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
 (DIR) commit 7689f88f24595ca9afe38cecab41bfc4e3c29473
 (DIR) parent 54deea3d092677422021864f23df5cb091f0a0ec
 (HTM) Author: Willy Goiffon <contact@z3bra.org>
       Date:   Mon,  5 Dec 2022 13:11:10 +0100
       
       Make board editable in-place
       
       Diffstat:
         M db.go                               |      58 +++++++++++++++++++++++++------
         M main.go                             |     173 +++++++------------------------
         M mkfile                              |       2 +-
         M playerbox.go                        |      12 ++++++------
         A ui.go                               |      86 ++++++++++++++++++++++++++++++
         M util.go                             |       2 +-
       
       6 files changed, 178 insertions(+), 155 deletions(-)
       ---
 (DIR) diff --git a/db.go b/db.go
       t@@ -38,15 +38,39 @@ const (
                `
        )
        
       -func db_count() int {
       +
       +func (a *Application) db_init(file string) error {
       +        var err error
       +
       +        // open database
       +        a.db, err = sql.Open("sqlite", file)
       +        if err != nil {
       +                return err
       +        }
       +
       +        // create schema if needed
       +        _, err = a.db.Exec(DB_CREATE)
       +        if err != nil {
       +                return err
       +        }
       +        /*
       +        _, err = a.db.Exec(DB_FILL)
       +        if err != nil {
       +                panic(err)
       +        }
       +        //*/
       +        return nil
       +}
       +
       +func (a *Application) db_count() int {
                var count int
                query := `SELECT count(id) FROM score;`
       -        row := db.QueryRow(query)
       +        row := a.db.QueryRow(query)
                row.Scan(&count)
                return count
        }
        
       -func db_rank(score int, ts int64) int {
       +func (a *Application) db_rank(score int, ts int64) int {
                var rank int
                query := `SELECT
                  count(id)
       t@@ -55,12 +79,26 @@ func db_rank(score int, ts int64) int {
                    score >= ?
                ;`
        
       -        row := db.QueryRow(query, score)
       +        row := a.db.QueryRow(query, score)
                row.Scan(&rank)
                return rank + 1
        }
        
       -func db_ranked(offset, limit int) ([]Player, error) {
       +func (a *Application) db_count_flag(flag int) int {
       +        var count int
       +        query := `SELECT
       +          count(id)
       +          FROM score
       +          WHERE
       +            flags >= ?
       +        ;`
       +
       +        row := a.db.QueryRow(query, flag)
       +        row.Scan(&count)
       +        return count
       +}
       +
       +func (a *Application) db_ranked(offset, limit int) ([]Player, error) {
                query := `SELECT
                  id,name,token,flag,score
                  FROM score
       t@@ -71,7 +109,7 @@ func db_ranked(offset, limit int) ([]Player, error) {
                  OFFSET ?
                ;`
        
       -        rows, err := db.Query(query, limit, offset)
       +        rows, err := a.db.Query(query, limit, offset)
                if err != nil {
                        return nil, err
                }
       t@@ -89,11 +127,11 @@ func db_ranked(offset, limit int) ([]Player, error) {
                return players, nil
        }
        
       -func db_player(id int) *Player {
       +func (a *Application) db_player(id int) *Player {
                var p Player
                query := `SELECT id,name,token,flag,score,ts FROM score WHERE id = ?`
        
       -        row := db.QueryRow(query, p.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
       t@@ -102,9 +140,9 @@ func db_player(id int) *Player {
                return &p
        }
        
       -func db_save(p Player) int64 {
       +func (a *Application) db_save(p Player) int64 {
                query := `INSERT INTO score(name,token,flag,score,ts) VALUES(?,?,?,?,?);`
       -        r, err := db.Exec(query, p.name, p.token, p.flags, p.score, p.ts)
       +        r, err := a.db.Exec(query, p.name, p.token, p.flags, p.score, p.ts)
                if err != nil {
                        panic(err)
                }
 (DIR) diff --git a/main.go b/main.go
       t@@ -17,21 +17,20 @@ package main
        import (
                "flag"
                "fmt"
       -        "math"
                "os"
                "regexp"
                "time"
                "database/sql"
                "github.com/gdamore/tcell/v2"
                "github.com/rivo/tview"
       -        "github.com/dustin/go-humanize"
                //"golang.org/x/crypto/argon2"
        
                _ "modernc.org/sqlite"
        )
        
        const (
       -        BOARD_SIZE int = 15
       +        BOARD_WIDTH int = 26
       +        BOARD_HEIGHT int = 15
                DB string = "leaderboard.db"
        )
        
       t@@ -45,10 +44,12 @@ type Player struct {
        }
        
        type Application struct {
       +        db *sql.DB
                app *tview.Application
                pages *tview.Pages
                frame *tview.Grid
       -        board *tview.Grid
       +        board *tview.Flex
       +        player Player
        }
        
        var flag_sha256 = [...]string {
       t@@ -59,20 +60,12 @@ var flag_sha256 = [...]string {
                "8CB250A66D4301244699186CA723E11AFA806C64466789AB14B7027A2F928BF8", // egg.png
                "F6A4071C9C0DDCCB53FC1CDCA38E32CF10551F75AA48996A9341842A7EF2591B"} // salt.png
        
       -//var Application {
       -//        app: tview.NewApplication(),
       -//        frame: BoardFrame(26, 15),
       -//        pages: tview.NewPages(),
       -//        board: tview.NewTable(),
       -//}
        
       -var app = tview.NewApplication()
       -var pages = tview.NewPages()
       -var db *sql.DB
       -var player = Player{
       -        score: 100,
       -        flags:  1,
       -        ts:  time.Now().Unix(),
       +var cyboard = Application{
       +        app: tview.NewApplication(),
       +        pages: tview.NewPages(),
       +        frame: tview.NewGrid(),
       +        board: tview.NewFlex(),
        }
        
        func usage() {
       t@@ -80,41 +73,6 @@ func usage() {
                os.Exit(0)
        }
        
       -func RankTable(t *tview.Table, offset, limit, rank int, fill bool) (*tview.Table) {
       -        players, err := db_ranked(offset, limit)
       -        if err != nil {
       -                panic(err)
       -        }
       -
       -        t.SetSelectable(true, false).
       -                SetSelectedStyle(tcell.StyleDefault.Reverse(true))
       -
       -        for i := 0; i < len(players); i++ {
       -                p := players[i]
       -                rankstr := fmt.Sprintf("%4s", humanize.Ordinal(rank + i))
       -                flagstr := flag2str(p.flags)
       -                scorestr := fmt.Sprintf("%4d", p.score)
       -                t.SetCell(i, 0, newcell(rankstr))
       -                t.SetCell(i, 1, newcell(p.name))
       -                t.SetCell(i, 2, newcell(flagstr))
       -                t.SetCell(i, 3, newcell(scorestr))
       -        }
       -
       -        if fill == true {
       -                for i:=t.GetRowCount(); i<limit; i++ {
       -                        rankstr  := fmt.Sprintf("%4s", humanize.Ordinal(rank + i))
       -                        flagstr  := "....."
       -                        scorestr := fmt.Sprintf("%4d", 0)
       -                        t.SetCell(i, 0, newcell(rankstr).SetTextColor(tcell.ColorGray))
       -                        t.SetCell(i, 1, newcell("AAA").SetTextColor(tcell.ColorGray))
       -                        t.SetCell(i, 2, newcell(flagstr).SetTextColor(tcell.ColorGray))
       -                        t.SetCell(i, 3, newcell(scorestr).SetTextColor(tcell.ColorGray))
       -                }
       -        }
       -
       -        return t
       -}
       -
        func pageFlag() tview.Primitive {
                input := tview.NewInputField().
                        SetLabel("SHA256(flag) ").
       t@@ -132,9 +90,9 @@ func pageFlag() tview.Primitive {
                input.SetDoneFunc(func(key tcell.Key) {
                                switch flagid(input.GetText()) {
                                case 0,1,2,3,4:
       -                                pages.SwitchToPage("score")
       +                                cyboard.pages.SwitchToPage("score")
                                default:
       -                                app.Stop()
       +                                cyboard.app.Stop()
                                        fmt.Println("Incorrect flag")
                                }
                        })
       t@@ -142,68 +100,14 @@ func pageFlag() tview.Primitive {
                return center(40, 1, input)
        }
        
       -func pageScore() tview.Primitive {
       -        board := tview.NewGrid().
       -                SetColumns(26).
       -                SetRows(1, BOARD_SIZE).
       -                SetBorders(true)
       -
       -        header := tview.NewTextView().
       -                SetDynamicColors(true).
       -                SetTextAlign(tview.AlignRight).
       -                SetText("[::b] RANK  NAME  FLAGS  SCORE ")
       -
       -        //count := db_count()
       -        rank := db_rank(player.score, player.ts)
       -
       -        grid := PlayerBoxGrid(player, rank)
       -
       -        //if (count < BOARD_SIZE - 1) {
       -                t1 := tview.NewTable()
       -                t2 := tview.NewTable()
       -                RankTable(t1, 0, rank-1, 1, false)
       -                RankTable(t2, rank, BOARD_SIZE - rank, rank + 1, true)
       -        //}
       -
       -        table := tview.NewFlex().
       -                SetDirection(tview.FlexRow).
       -                AddItem(t1, rank - 1, 0, false).
       -                AddItem(grid, 1, 0, true).
       -                AddItem(t2, BOARD_SIZE - rank - 1, 0, false)
       -
       -        board.AddItem(header, 0, 0, 1, 1, 1, 26, false)
       -        board.AddItem(table, 1, 0, 1, 1, 1, 26, true)
       -
       -        return center(30, 20, board)
       -}
       -
        func pageBoard() tview.Primitive {
       -        bsize := int(math.Max(float64(db_count()), float64(BOARD_SIZE)))
       -        board := tview.NewGrid().
       -                SetColumns(26).
       -                SetRows(1, bsize).
       -                SetBorders(true)
       -
       -        header := tview.NewTextView().
       -                SetDynamicColors(true).
       -                SetTextAlign(tview.AlignRight).
       -                SetText("[::b] RANK  NAME  FLAGS  SCORE ")
       -
       -        rank := db_rank(player.score, time.Now().Unix())
       -
       -        table := tview.NewTable()
       -        RankTable(table, 0, bsize, 1, true)
       -        table.SetOffset(rank, 0)
       -
       -        board.AddItem(header, 0, 0, 1, 1, 1, 26, false)
       -        board.AddItem(table, 1, 0, 1, 1, 1, 26, true)
       +        cyboard.SetupFrame()
       +        cyboard.DrawBoard()
        
       -        return center(30, 20, board)
       +        return center(30, 20, cyboard.frame)
        }
        
        func main() {
       -        var err error
       -
                cmd := "board"
        
                flag.Parse()
       t@@ -227,51 +131,46 @@ func main() {
                tview.Styles.GraphicsColor            = tcell.ColorDefault
                tview.Styles.PrimaryTextColor         = tcell.ColorDefault
        
       -        // open database
       -        db, err = sql.Open("sqlite", DB)
       -        if err != nil {
       -                panic(err)
       -        }
       -        defer db.Close()
       +        cyboard.db_init(DB)
       +        defer cyboard.db.Close()
        
       -        // create schema if needed
       -        _, err = db.Exec(DB_CREATE)
       -        if err != nil {
       -                panic(err)
       -        }
       -        //_, err = db.Exec(DB_FILL)
       -        //if err != nil {
       -        //        panic(err)
       -        //}
       -        pages.SetBackgroundColor(tcell.ColorDefault)
       +        cyboard.pages.SetBackgroundColor(tcell.ColorDefault)
        
       -        pages.AddPage("flag",   pageFlag(), true, false)
       -        pages.AddPage("score",  pageScore(), true, false)
       -        pages.AddPage("board",  pageBoard(), true, false)
       +        cyboard.pages.AddPage("flag",   pageFlag(), true, false)
       +        cyboard.pages.AddPage("board",  pageBoard(), true, false)
        
                switch cmd {
       -        case "prompt":
       -                pages.ShowPage("flag")
                case "flag":
                        if (len(args) < 2) {
                                usage()
                        }
                        switch flagid(args[1]) {
                        case 0:
       -                        if db_count() < 10 {
       -                                player.score += 10 - db_count()
       +                        cyboard.player.flags = 1
       +                        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()
       +                        if n < 10 {
       +                                cyboard.player.score += 10 - n
                                }
       -                        pages.SwitchToPage("score")
       +
       +                        rank := cyboard.db_rank(cyboard.player.score, cyboard.player.ts)
       +
       +                        cyboard.NewPlayer(rank)
       +                        cyboard.pages.SwitchToPage("board")
                        //case 1,2,3,4:
                        //        pages.ShowPage("token")
                        default:
                                fmt.Println("Incorrect flag")
                        }
                default:
       -                pages.SwitchToPage("board")
       +                cyboard.DrawBoard()
       +                cyboard.pages.SwitchToPage("board")
                }
        
       -        if err := app.SetRoot(pages, true).EnableMouse(true).Run(); err != nil {
       +        if err := cyboard.app.SetRoot(cyboard.pages, true).EnableMouse(true).Run(); err != nil {
                        panic(err)
                }
        }
 (DIR) diff --git a/mkfile b/mkfile
       t@@ -1,4 +1,4 @@
        GO = go
        
       -scoreboard: main.go db.go playerbox.go util.go
       +scoreboard: main.go db.go playerbox.go util.go ui.go
                ${GO} build
 (DIR) diff --git a/playerbox.go b/playerbox.go
       t@@ -24,7 +24,7 @@ func boxtext (b PlayerBox) string {
                for i:=0; i<3; i++ {
                        b.name[i] = charlist[b.char[i]]
                        if i == b.cur {
       -                        str = fmt.Sprintf("%s[::r]%c[::-]", str, b.name[i])
       +                        str = fmt.Sprintf("%s[::rl]%c[::-]", str, b.name[i])
                        } else {
                                str = fmt.Sprintf("%s%c", str, b.name[i])
                        }
       t@@ -65,18 +65,18 @@ func PlayerBoxName(p Player) *tview.TextView {
                        SetTextAlign(tview.AlignRight).
                        SetText(boxtext(playerbox)).
                        SetChangedFunc(func() {
       -                        app.Draw()
       +                        cyboard.app.Draw()
                        }).
                        SetDoneFunc(func(key tcell.Key) {
                                if key == tcell.KeyEnter {
                                        p.name = fmt.Sprintf("%3s", playerbox.name)
       -                                db_save(p)
       -                                pages.SwitchToPage("board")
       +                                cyboard.db_save(p)
       +                                cyboard.pages.SwitchToPage("board")
                                }
                        })
        
                v.Focus(func(p tview.Primitive) {
       -                v.SetText(fmt.Sprintf("%4d ", player.score))
       +                v.SetText(fmt.Sprintf("%4d ", cyboard.player.score))
                })
        
                v.SetInputCapture(manipulatebox)
       t@@ -89,7 +89,7 @@ func PlayerBoxGrid(p Player, rank int) *tview.Grid {
                        return tview.NewTextView().
                                SetDynamicColors(true).
                                SetTextAlign(tview.AlignRight).
       -                        SetText(fmt.Sprintf("[::l]%s",text))
       +                        SetText(fmt.Sprintf("[::b]%s",text))
                }
        
                rankstr := humanize.Ordinal(rank)
 (DIR) diff --git a/ui.go b/ui.go
       t@@ -0,0 +1,86 @@
       +package main
       +
       +import (
       +        "fmt"
       +        "math"
       +        "github.com/gdamore/tcell/v2"
       +        "github.com/rivo/tview"
       +        "github.com/dustin/go-humanize"
       +)
       +
       +func BoardHeader() *tview.TextView {
       +        return tview.NewTextView().
       +                SetDynamicColors(true).
       +                SetTextAlign(tview.AlignRight).
       +                SetText("[::b] RANK  NAME  FLAGS  SCORE ")
       +}
       +
       +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)
       +        if err != nil {
       +                panic(err)
       +        }
       +
       +        t.SetSelectable(true, false).
       +                SetSelectedStyle(tcell.StyleDefault.Reverse(true))
       +
       +        for i := 0; i < len(players); i++ {
       +                p := players[i]
       +                rankstr := fmt.Sprintf("%4s", humanize.Ordinal(rank + i + 1))
       +                flagstr := flag2str(p.flags)
       +                scorestr := fmt.Sprintf("%4d", p.score)
       +                t.SetCell(i, 0, newcell(rankstr))
       +                t.SetCell(i, 1, newcell(p.name))
       +                t.SetCell(i, 2, newcell(flagstr))
       +                t.SetCell(i, 3, newcell(scorestr))
       +        }
       +
       +        if fill == true {
       +                bsize := int(math.Max(float64(BOARD_HEIGHT), float64(limit)))
       +                for i:=t.GetRowCount(); i<bsize; i++ {
       +                        rankstr  := fmt.Sprintf("%4s", humanize.Ordinal(rank + i + 1))
       +                        flagstr  := "....."
       +                        scorestr := fmt.Sprintf("%4d", 0)
       +                        t.SetCell(i, 0, newcell(rankstr).SetTextColor(tcell.ColorGray))
       +                        t.SetCell(i, 1, newcell("AAA").SetTextColor(tcell.ColorGray))
       +                        t.SetCell(i, 2, newcell(flagstr).SetTextColor(tcell.ColorGray))
       +                        t.SetCell(i, 3, newcell(scorestr).SetTextColor(tcell.ColorGray))
       +                }
       +        }
       +
       +        return t
       +}
       +
       +
       +func (a *Application) SetupFrame() {
       +        cyboard.frame.
       +                SetColumns(BOARD_WIDTH).
       +                SetRows(1, BOARD_HEIGHT).
       +                SetBorders(true)
       +
       +        cyboard.frame.AddItem(BoardHeader(), 0, 0, 1, 1, 1, BOARD_WIDTH, false)
       +        cyboard.frame.AddItem(cyboard.board, 1, 0, 1, 1, BOARD_HEIGHT, BOARD_WIDTH, true)
       +}
       +
       +func (a *Application) DrawBoard() {
       +        cyboard.board.Clear().
       +                SetDirection(tview.FlexRow).
       +                AddItem(RankTable(0, -1, 0, true), 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)
       +        box := PlayerBoxGrid(cyboard.player, rank)
       +
       +        cyboard.board.Clear().
       +                SetDirection(tview.FlexRow).
       +                AddItem(t1, rank-1, 0, false).
       +                AddItem(box, 1, 0, true).
       +                AddItem(t2, BOARD_HEIGHT - rank, 0, false)
       +}
       +
 (DIR) diff --git a/util.go b/util.go
       t@@ -43,7 +43,7 @@ func flag2str(n int) string {
        func newcell(text string) *tview.TableCell {
                return tview.NewTableCell(text).
                        SetTransparency(true).
       -                SetSelectable(false).
       +                SetSelectable(true).
                        SetAlign(tview.AlignRight).
                        SetExpansion(1)
        }