diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..6b1708d --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,6 @@ +engines: + markdownlint: + enabled: true + checks: + MD013: + enabled: false diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4b35c88..0db5f48 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -48,8 +48,8 @@ jobs: run: go vet -tags ci ./... - name: goimports - run: test -z $(goimports -e -d . | tee /dev/stderr) - + run: bash -c "2>&1 goimports -e -d ." + - name: gocyclo run: gocyclo -over 50 . @@ -64,32 +64,32 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 - # coverage: - # runs-on: ubuntu-latest - # steps: - # - name: Setup Golang 1.15.x - # if: success() - # uses: actions/setup-go@v2 - # with: - # go-version: '^1.15.x' + coverage: + runs-on: ubuntu-latest + steps: + - name: Setup Golang 1.15.x + if: success() + uses: actions/setup-go@v2 + with: + go-version: '^1.15.x' - # - name: Checkout repository - # uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v2 - # - name: Get dependencies - # run: | - # sudo apt-get update - # sudo apt-get install gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev + - name: Get dependencies + run: | + sudo apt-get update + sudo apt-get install gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev - # - name: Calc coverage - # run: | - # go test -v -covermode=count -coverprofile=coverage.out ./... + - name: Calc coverage + run: | + go test -v -covermode=count -coverprofile=coverage.out ./... - # - name: Convert coverage.out to coverage.lcov - # uses: jandelgado/gcov2lcov-action@v1.0.6 + - name: Convert coverage.out to coverage.lcov + uses: jandelgado/gcov2lcov-action@v1.0.6 - # - name: Coveralls GitHub Action - # uses: coverallsapp/github-action@v1.1.2 - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} - # path-to-lcov: coverage.lcov + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: coverage.lcov diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b9eee01 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch test package", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/cmd" + }, + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/cmd" + }, + { + "name": "Launch dlv", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${workspaceFolder}/cmd", + "env": {}, + "args": [], + "showLog": true, + }, + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..70c3211 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## Unreleased + +**Added** + +- Button allows you to undo any value set. +- Simple mistakes are highlighted as you play. (#4) +- Button to check for mistakes. (#4) +- Set/Clear values with alpha keys/numpad. (#5) +- Button to reset puzzle to initial state. (#20) +- New Game Timer shown in the bottom right, stops when puzzle solved. (#27) + +**Changed** + +- More uniform Focus/Hover/Selected colours. + +**Fixed** + +- Many slowdowns in check methods. (#11) +- Bug where boxes showed cells in the wrong order in a different orientation. (#21, #22) +- Puzzle Check button now also tells you if you have found a possible solution. (#27) + +## v0.1 + +- Initial Tagged Build +- Support for Classic Sodoku (9x9). +- Drag-select multiple boxes. diff --git a/Makefile b/Makefile index edd2714..449818c 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ MAKE = make --no-print-directory FYNE_CROSS = $(shell go env | awk -F'"' '/GOPATH/ {print $$2}')/bin/fyne-cross -DESCRIBE := $(shell go run gen.go VERSION) +DESCRIBE := $(shell go run cmd/gen.go VERSION) DESCRIBE_PARTS := $(subst -, ,$(DESCRIBE)) VERSION_TAG := $(word 1,$(DESCRIBE_PARTS)) @@ -19,8 +19,8 @@ NEXT_MAJOR := $(shell echo $$(($(MAJOR)+1))) NEXT_MINOR := $(shell echo $$(($(MINOR)+1))) NEXT_MICRO = $(shell echo $$(($(MICRO)+$(COMMITS_SINCE_TAG)))) -BINARYNAME = $(shell go run gen.go PROGRAM) -MODNAME = github.com/AlbinoGeek/$(BINARYNAME) +BINARYNAME = $(shell go run cmd/gen.go PROGRAM) +MODNAME = github.com/AlbinoGeek/$(BINARYNAME)/cmd APP_NAME = com.github.albinogeek.$(BINARYNAME) TARGETDIR = _dist diff --git a/README.md b/README.md index fb478eb..a865d8c 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,47 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/be0523753694eee85927/maintainability)](https://codeclimate.com/github/AlbinoGeek/number-place/maintainability) [![CI](https://github.com/AlbinoGeek/number-place/workflows/CI/badge.svg?branch=main)](#) [![GoReportCard](https://goreportcard.com/badge/github.com/AlbinoGeek/number-place)](https://goreportcard.com/report/github.com/AlbinoGeek/number-place) +[![Coverage Status](https://coveralls.io/repos/github/AlbinoGeek/number-place/badge.svg?branch=develop)](https://coveralls.io/github/AlbinoGeek/number-place?branch=develop) + +[![](cmd/testdata/start.png)](cmd/testdata/start.png) ## Features +Share puzzles simply by copying a string: + +``` +3,3,3,3,53-6---98-7-195----------6-8--4--7---6-8-3-2---3--1--6-6----------419-8-28---5-79 +``` + +Cells: + +- Click/Tap Toggle Select +- Drag to Select Multiple +- Unlimited Undo Levels + +Help: + +- Highlights Simple Mistakes + +### Keyboard Controls + +With Cells Selected: + +- Use the alpha keys or numpad to set the center value +- Use the `del`ete or `backspace` keys to clear all values + ### Puzzles Supported -- [ ] Sodoku - - [ ] Standard Grid (9x9, 3x3 subgrids) - - [ ] Classic - - [ ] Mini Grid (6x6, 3x2 subgrids) - - [ ] Giant Grid (16x16, 4x4 subgrids) -- [ ] Constrainted-Based Gridded Sodoku: - - [ ] Standard Grid (9x9, 3x3 subgrids) +- [X] Sudoku + - [X] Standard Grid (9x9, 3x3 boxes) + - [X] Classic + - [ ] Quadratum latinum (uses roman numerals for values) + - [ ] Alphabetical (uses a key of letters for values) + - [ ] Mini Grid (6x6, 3x2 boxes) + - [ ] Giant Grid (16x16, 4x4 boxes) +- [ ] Constrainted-Based Gridded Sudoku: + - [ ] Standard Grid (9x9, 3x3 boxes) + - [ ] "Killer" - [ ] Greater Than - [ ] XV diff --git a/cmd/board.go b/cmd/board.go new file mode 100644 index 0000000..6a4d7c0 --- /dev/null +++ b/cmd/board.go @@ -0,0 +1,471 @@ +package main + +import ( + "errors" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + jsoniter "github.com/json-iterator/go" + "github.com/kataras/golog" +) + +type state struct { + Data []byte + Name string + Time int64 +} + +type board struct { + *fyne.Container `json:"-"` + gameTimer *canvas.Text + gameDuration binding.String + + solved binding.Bool + timeStart time.Time + timeFinish time.Time + + mu sync.Mutex + cells []*cell + history []*state + initial *state + + boxWidth int + boxHeight int + boxesWide int + boxesTall int + + // optimization: pre-calculated fields (used often) + cellsPerBox int + cellsPerCol int + cellsPerRow int +} + +func newBoard(boxWidth, boxHeight, boxesWide, boxesTall int) *board { + var b = &board{ + gameTimer: canvas.NewText("", theme.ForegroundColor()), + gameDuration: binding.NewString(), + boxWidth: boxWidth, + boxHeight: boxHeight, + boxesWide: boxesWide, + boxesTall: boxesTall, + cellsPerBox: boxWidth * boxHeight, + cellsPerCol: boxHeight * boxesTall, + cellsPerRow: boxWidth * boxesWide, + solved: binding.NewBool(), + } + + b.gameTimer.Hide() + + timeFormat := func(t time.Duration) string { + return fmt.Sprintf("%.1fs", t.Seconds()) + } + + b.gameDuration.AddListener(binding.NewDataListener(func() { + last, _ := b.gameDuration.Get() + end := time.Now() + if b.timeFinish.UnixNano() > b.timeStart.UnixNano() { + end = b.timeFinish + } + + // ! recursive data binding refresh, is this safe/sane? + // ? maybe this should be triggered by animation instead + go func() { + time.Sleep(time.Millisecond * 125) + if s := timeFormat(end.Sub(b.timeStart)); s != last { + b.gameTimer.Text = s + b.gameTimer.Refresh() + + b.gameDuration.Set(s) + } + }() + })) + + b.solved.AddListener(binding.NewDataListener(func() { + solved, _ := b.solved.Get() + b.mu.Lock() + for _, c := range b.cells { + if solved { + c.Disable() + } else { + c.Enable() + } + } + b.mu.Unlock() + })) + + b.init() + return b +} + +// UndoIndex is used by the undo function to determine the target state. +type UndoIndex int + +var ( + // InitialState will trigger undo to return the board to its' initial state, + // that is, what the board looked like after it was last loaded. + InitialState UndoIndex = -2 + + // RecentState will trigger undo to return the board before the last change + // had taken place. This is akin to a traditional undo feature. + RecentState UndoIndex = -1 +) + +func (b *board) Reset() { + if b.initial != nil { + b.undo(-2) + } +} + +func (b *board) Solved() bool { + if solved, _ := b.solved.Get(); solved { + return true + } + + if b.check() != nil { + return false + } + + for _, c := range b.cells { + if c.Given == "" && c.Center == "" { + return false + } + } + + b.timeFinish = time.Now() + b.solved.Set(true) + return true +} + +func (b *board) Undo() { + b.undo(RecentState) +} + +// ! BADLY NAMED +type checkerCallback func(dupes []*cell) + +// ! BADLY NAMED +type checker func(duplicates checkerCallback) error + +func (b *board) check() error { + b.mu.Lock() + + var cb checkerCallback + if HighlightMistakes { + cb = b.setMistakes(true) + } + + errors := make([]error, 0) + for _, f := range []checker{ + b.checkBoxRepeat, + b.checkColRepeat, + b.checkRowRepeat, + } { + if err := f(cb); err != nil { + errors = append(errors, err) + } + } + + b.mu.Unlock() + + if len(errors) > 0 { + return errors[0] + } + + return nil +} + +func (b *board) setMistakes(v bool) func([]*cell) { + return func(cells []*cell) { + for _, c := range cells { + c.SetMistake(v) + } + } +} + +// checkBoxRepeat checks the constraint : No value may be repeated within a box +func (b *board) checkBoxRepeat(duplicates checkerCallback) (err error) { + for box := 0; box < b.boxesTall*b.boxesWide; box++ { + cellIDs := b.getBox(box) + + if items := checkDuplicateIDs(b, cellIDs); len(items) > 0 { + err = fmt.Errorf("box %d contains duplicate values", 1+box) + if duplicates != nil { + duplicates(items) + } + } + } + + return err +} + +// checkColRepeat checks the constraint : No value may be repeated within a column +func (b *board) checkColRepeat(duplicates checkerCallback) (err error) { + var ( + cellIDs = make([]int, b.cellsPerCol) + + col, colNum int + ) + + for col = 0; col < b.cellsPerRow; col++ { + for i := 0; i < b.cellsPerCol; i++ { + cellIDs[i] = col + i*b.cellsPerCol + } + + colNum++ + if items := checkDuplicateIDs(b, cellIDs); len(items) > 0 { + err = fmt.Errorf("column %d contains duplicate values", colNum) + if duplicates != nil { + duplicates(items) + } + } + } + + return err +} + +// checkRowRepeat checks the constraint : No value may be repeated within a row +func (b *board) checkRowRepeat(duplicates checkerCallback) (err error) { + var ( + cellIDs = make([]int, b.cellsPerRow) + + row, rowNum, offset int + ) + + for row = 0; row < b.cellsPerCol; row++ { + offset = b.cellsPerRow * row + for i := 0; i < b.cellsPerRow; i++ { + cellIDs[i] = offset + i + } + + rowNum++ + if items := checkDuplicateIDs(b, cellIDs); len(items) > 0 { + err = fmt.Errorf("row %d contains duplicate values", rowNum) + if duplicates != nil { + duplicates(items) + } + } + } + + return err +} + +func checkDuplicateIDs(b *board, ids []int) []*cell { + var ( + occur = make(map[string][]int) + value string + ) + + for _, c := range ids { + if value = b.cells[c].Given; value == "" { + if value = b.cells[c].Center; value == "" { + continue + } + } + + if _, exist := occur[value]; !exist { + occur[value] = []int{} + } + + occur[value] = append(occur[value], c) + } + + dupes := make([]*cell, 0) + for _, o := range occur { + if len(o) > 1 { + for _, c := range o { + dupes = append(dupes, b.cells[c]) + } + } + } + + return dupes +} + +func (b *board) getBox(box int) (cells []int) { + cells = make([]int, b.cellsPerBox) + + boxy := box / b.boxesWide + boxx := box - boxy*b.boxesWide + offset := boxy*b.cellsPerRow*b.boxHeight + boxx*b.boxWidth + + for i, y := 0, 0; y < b.boxHeight; y++ { + for x := 0; x < b.boxWidth; x++ { + cells[i] = y*b.cellsPerRow + x + offset + i++ + } + } + + + return +} + +func (b *board) init() { + b.mu.Lock() + defer b.mu.Unlock() + + b.history = make([]*state, 0) + b.timeStart = time.Now() + + b.solved.Set(false) + + var ( + // TODO: support other cell arrangements, counts, in an elegant way + numBoxes = b.boxesWide * b.boxesTall + boxObjects []fyne.CanvasObject + ) + + // optimization: reduce allocations by re-using previously-created fyne.Container + if b.Container != nil { + boxObjects = b.Container.Objects + + if have := len(boxObjects); have > numBoxes || cap(boxObjects) >= numBoxes { + // len too high, or len too low, but space in capacity + boxObjects = boxObjects[:numBoxes] + } else if have < numBoxes { + // len too low, needs extend + boxObjects = append(boxObjects, make([]fyne.CanvasObject, numBoxes-have)...) + } + } else { + boxObjects = make([]fyne.CanvasObject, numBoxes) + } + + b.cells = make([]*cell, b.cellsPerBox*numBoxes) + for i := range b.cells { + b.cells[i] = newCell(i) + } + + for i := 0; i < numBoxes; i++ { + cells := make([]fyne.CanvasObject, b.cellsPerBox) + for j, k := range b.getBox(i) { + cells[j] = b.cells[k] + } + + if boxObjects[i] != nil { + // optimization: reduce allocations by re-using previously-created widget.Card + box := boxObjects[i].(*widget.Card).Content.(*fyne.Container) + box.Objects = cells + box.Refresh() + } else { + boxObjects[i] = widget.NewCard("", "", fyne.NewContainerWithLayout( + layout.NewGridLayout(b.boxWidth), + cells..., + )) + } + } + + if b.Container != nil { + // optimization: reduce allocations by re-using previously created fyne.Container + b.Container.Objects = boxObjects + b.Container.Refresh() + } else { + b.Container = fyne.NewContainerWithLayout( + layout.NewGridLayout(b.boxesWide), + boxObjects..., + ) + } +} + +func (b *board) load(in string) error { + parts := strings.Split(in, ",") + + if len(parts) != 5 { + return errors.New("bad format") + } + + p, err := strconv.Atoi(parts[0]) + if err != nil { + return fmt.Errorf("bad boxWidth should be integer") + } + b.boxWidth = p + + if p, err = strconv.Atoi(parts[1]); err != nil { + return fmt.Errorf("bad boxHeight should be integer") + } + b.boxHeight = p + + if p, err = strconv.Atoi(parts[2]); err != nil { + return fmt.Errorf("bad boxesWide should be integer") + } + b.boxesWide = p + + if p, err = strconv.Atoi(parts[3]); err != nil { + return fmt.Errorf("bad boxesTall should be integer") + } + b.boxesTall = p + + b.cellsPerBox = b.boxWidth * b.boxHeight + b.cellsPerCol = b.boxHeight * b.boxesTall + b.cellsPerRow = b.boxWidth * b.boxesWide + + b.init() + + if a, b := len(parts[4]), len(b.cells); a != b { + return fmt.Errorf("bad data has wrong cell count: expected %d, got %d", b, a) + } + + b.mu.Lock() + for i, c := range b.cells { + if v := parts[4][i]; v != '-' { + c.SetGiven(string(v)) + } + } + b.mu.Unlock() + + b.registerUndo() + b.initial = b.history[0] + + return b.check() +} + +func (b *board) registerUndo() { + b.mu.Lock() + defer b.mu.Unlock() + + data, err := jsoniter.Marshal(&b.cells) + if err != nil { + golog.Fatalf("unable to save state: %v", err) + } + + b.history = append(b.history, &state{ + Data: data, + Name: "", + Time: time.Now().Unix(), + }) +} + +func (b *board) undo(idx UndoIndex) { + if len(b.history) == 0 { + return + } + + b.mu.Lock() + + mod := int(idx) + if l := len(b.history); idx == RecentState || mod > l { + mod = l - 1 + } + + var s *state + + if idx == InitialState { + s = b.initial + } else { + s = b.history[mod] + b.history = b.history[:mod] + } + + jsoniter.Unmarshal(s.Data, &b.cells) + b.setMistakes(false)(b.cells) + b.mu.Unlock() + + b.check() +} diff --git a/cmd/board_test.go b/cmd/board_test.go new file mode 100644 index 0000000..04cdcaf --- /dev/null +++ b/cmd/board_test.go @@ -0,0 +1,266 @@ +package main + +import ( + "fmt" + "math/rand" + "strconv" + "testing" + "time" + + "fyne.io/fyne/v2/test" + + "github.com/stretchr/testify/assert" +) + +// tests +var ( + testBoxRepeat = `3,3,3,3,55-6---98-7-195----------6-8--4--7---6-8-3-2---3--1--6-6----------419-8-28---5-79` + testColRepeat = `3,3,3,3,53-6---98-7-195----------6-5--4--7---6-8-3-2---3--1--6-6----------419-8-28---5-79` + testRowRepeat = `3,3,3,3,53-6---9857-19-----------6-8--4--7---6-8-3-2---3--1--6-6----------419-8-28---5-79` + testEasySolve = `2,2,2,2,-234341223414123` +) + +func TestBoardLoadCheck(t *testing.T) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + assert.NoError(t, board.load(wikipedia), "failed loading valid classic sudoku") + assert.Errorf(t, board.load(testBoxRepeat), "loading repeats in boxes should have failed") + assert.Errorf(t, board.load(testColRepeat), "loading repeats in col should have failed") + assert.Errorf(t, board.load(testRowRepeat), "loading repeats in row should have failed") +} + +func TestBoardGetBox(t *testing.T) { + var ( + a = test.NewApp() + board, w = start(a) + ) + a.Run() + + for _, tc := range []struct { + box int + data string + }{ + {0, `3,3,3,3,123------456------789------------------------------------------------------------`}, + {1, `3,3,3,3,---123------456------789---------------------------------------------------------`}, + {2, `3,3,3,3,------123------456------789------------------------------------------------------`}, + {3, `3,3,3,3,---------------------------123------456------789---------------------------------`}, + {4, `3,3,3,3,------------------------------123------456------789------------------------------`}, + {5, `3,3,3,3,---------------------------------123------456------789---------------------------`}, + {6, `3,3,3,3,------------------------------------------------------123------456------789------`}, + {7, `3,3,3,3,---------------------------------------------------------123------456------789---`}, + {8, `3,3,3,3,------------------------------------------------------------123------456------789`}, + } { + assert.NoError(t, board.load(tc.data)) + for i, c := range board.getBox(tc.box) { + assert.Equal(t, strconv.Itoa(1+i), board.cells[c].Given) + } + + test.AssertImageMatches(t, + fmt.Sprintf("board-get-box-%d.png", tc.box), + w.Canvas().Capture()) + } +} + +func TestBoardLoadInvalid(t *testing.T) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + for _, s := range []string{ + "1,1,1,1,", + "foo", + ",,,,", + "1,,foo,,", + "1,1,,,foo", + "1,1,1,,foo", + } { + assert.Error(t, board.load(s)) + } + + assert.NoError(t, board.load(`1,1,1,1,1`), "TECHNICALLY this should load") +} + +func TestBoardSolved(t *testing.T) { + _ = test.NewApp() + + board := newBoard(2, 2, 2, 2) + + assert.NoError(t, board.load(wikipedia), "failed loading valid classic sudoku") + + assert.Equal(t, false, board.Solved(), "puzzle with blanks should not be solved") + + assert.Errorf(t, board.load(testBoxRepeat), "loading repeats in boxes should have failed") + + assert.Equal(t, false, board.Solved(), "invalid puzzle should not be solved") + + assert.NoError(t, board.load(testEasySolve), "failed loading easy solve") + + assert.Equal(t, false, board.Solved(), "easy solve should not be pre-solved") + + // insure positive solve time + time.Sleep(time.Millisecond) + + board.cells[0].SetCenter("1") + + assert.Equal(t, true, board.Solved(), "easy solve should now be solved") + + assert.Greater(t, board.timeFinish.UnixNano(), board.timeStart.UnixNano(), "finish time should be positive") +} + +func TestBoardInitExpand(t *testing.T) { + _ = test.NewApp() + + board := newBoard(2, 2, 2, 2) + + assert.NoError(t, board.load(wikipedia), "failed loading valid classic sudoku") +} + +func TestBoardUndo(t *testing.T) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + assert.NoError(t, board.load(wikipedia), "failed loading valid classic sudoku") + + board.registerUndo() + state := board.history[0] + + cell := board.cells[2] + old := cell.Center + + // It's our responsibility to save a history state if we're setting manually + board.registerUndo() + cell.SetCenter("5") + + assert.Errorf(t, board.check(), "checking repeats in boxes should have failed") + assert.EqualValues(t, cell.Center, "5", "SetCenter failed to set expected value") + + board.Undo() + assert.EqualValues(t, cell.Center, old, "undo did not restore value") + + // Set many values + for _, i := range []int{9, 18, 14, 27, 31, 2, 51, 60, 73} { + v := strconv.Itoa(rand.Intn(9)) + + board.registerUndo() + board.cells[i].SetCenter(v) + + assert.EqualValues(t, v, board.cells[i].Center, "SetCenter failed to set expected value") + } + + board.Reset() + board.registerUndo() + assert.Equal(t, state, board.history[len(board.history)-1], "initial state undo did not work") +} + +func BenchmarkBoardCheckBoxes(b *testing.B) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + assert.NoError(b, board.load(wikipedia), "failed loading valid classic sudoku") + + cb := func(cells []*cell) { + assert.Equal(b, len(cells), 0) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + board.checkBoxRepeat(cb) + } +} + +func TestBoardCheckBoxesExhaustive(t *testing.T) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + var offset int + var ic, jc *cell + for box := 0; box < board.boxesWide*board.boxesTall; box++ { + offset = box * board.cellsPerBox + + // ! should do half as many comparisons, but I need to test which ones + for i := 0; i < board.cellsPerBox; i++ { + for j := 0; j < board.cellsPerBox; j++ { + if i == j { + continue + } + + ic = board.cells[offset+i] + jc = board.cells[offset+j] + ic.SetCenter("5") + jc.SetCenter("5") + assert.Error(t, board.check(), "failed check for duplicate (%d, %d) in box %d", i, j, box) + + ic.SetCenter("") + jc.SetCenter("") + assert.NoError(t, board.check()) + } + } + } +} +func BenchmarkBoardCheckCols(b *testing.B) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + assert.NoError(b, board.load(wikipedia), "failed loading valid classic sudoku") + + cb := func(cells []*cell) { + assert.Equal(b, len(cells), 0) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + board.checkColRepeat(cb) + } +} + +func BenchmarkBoardCheckRows(b *testing.B) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + assert.NoError(b, board.load(wikipedia), "failed loading valid classic sudoku") + + cb := func(cells []*cell) { + assert.Equal(b, len(cells), 0) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + board.checkRowRepeat(cb) + } +} + +func BenchmarkBoardCheck(b *testing.B) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + assert.NoError(b, board.load(wikipedia), "failed loading valid classic sudoku") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + board.check() + } +} + +func BenchmarkBoardLoad(b *testing.B) { + _ = test.NewApp() + + board := newBoard(3, 3, 3, 3) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + board.load(wikipedia) + } +} diff --git a/cmd/cell.go b/cmd/cell.go new file mode 100644 index 0000000..67ca107 --- /dev/null +++ b/cmd/cell.go @@ -0,0 +1,222 @@ +package main + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var _ fyne.Widget = (*cell)(nil) +var _ fyne.Disableable = (*cell)(nil) +var _ desktop.Hoverable = (*cell)(nil) +var _ desktop.Mouseable = (*cell)(nil) +var _ mobile.Touchable = (*cell)(nil) + +type cell struct { + widget.BaseWidget `json:"-"` + + ID int + Center string + Given string + + disabled bool + hovered bool + mistake bool + selected bool +} + +func newCell(id int) *cell { + // TODO: implement corner numbers + // c := fyne.NewContainerWithLayout( + // layout.NewGridLayout(3), + // ) + + c := &cell{ID: id} + c.ExtendBaseWidget(c) + + return c +} + +func (c *cell) CreateRenderer() fyne.WidgetRenderer { + rend := &cellRenderer{ + cell: c, + rect: canvas.NewRectangle(color.Transparent), + text: canvas.NewText(c.Center, theme.ForegroundColor()), + } + + rend.rect.StrokeWidth = 1 + rend.rect.StrokeColor = theme.ShadowColor() + rend.text.Alignment = fyne.TextAlignCenter + rend.text.TextSize = 20 + rend.text.TextStyle.Monospace = true + + return rend +} + +func (c *cell) Disable() { + c.disabled = true + c.Refresh() +} + +func (c *cell) Disabled() bool { + return c.disabled +} + +func (c *cell) Enable() { + c.disabled = false + c.Refresh() +} + +func (c *cell) MouseIn(evt *desktop.MouseEvent) { + if !c.Readonly() { + c.hovered = true + + if evt.Button == desktop.LeftMouseButton && downCell != c { + wasSelected = false // reset drag event + downCell = c + c.Select() + return + } + + c.Refresh() + } +} + +func (c *cell) MouseOut() { + if !c.Readonly() { + c.hovered = false + c.Refresh() + } +} + +// TODO: MOVE THIS, RENAME THIS +var downCell *cell +var wasSelected bool + +func (c *cell) MouseMoved(*desktop.MouseEvent) {} + +func (c *cell) MouseDown(*desktop.MouseEvent) { + downCell = c + wasSelected = c.selected + + c.Select() +} + +func (c *cell) MouseUp(*desktop.MouseEvent) { + if downCell == c && wasSelected { + c.selected = false + c.Refresh() + } + + downCell = nil + wasSelected = false +} + +func (c *cell) TouchDown(*mobile.TouchEvent) { + c.MouseDown(nil) +} + +func (c *cell) TouchUp(*mobile.TouchEvent) { + c.MouseUp(nil) +} + +func (c *cell) TouchCancel(*mobile.TouchEvent) {} + +// --- + +func (c *cell) Readonly() bool { + return c.Given != "" || c.disabled +} + +func (c *cell) Select() { + if !c.Readonly() && !c.selected { + c.selected = true + c.Refresh() + } +} + +func (c *cell) SetGiven(n string) { + c.mistake = false + c.Given = n + c.Refresh() +} + +func (c *cell) SetCenter(n string) { + c.mistake = false + c.Center = n + c.Refresh() +} + +func (c *cell) SetMistake(b bool) { + if c.mistake != b { + c.mistake = b + c.Refresh() + } +} + +type cellRenderer struct { + cell *cell + rect *canvas.Rectangle + text *canvas.Text +} + +func (r *cellRenderer) BackgroundColor() color.Color { + return color.Transparent +} + +func (r *cellRenderer) Destroy() {} + +func (r *cellRenderer) Layout(space fyne.Size) { + r.rect.Resize(space) + + tSize := r.text.MinSize() + r.text.Move(fyne.NewPos(0, space.Height/2-tSize.Height/2)) + r.text.Resize(fyne.NewSize(space.Width, tSize.Height)) +} + +func (r *cellRenderer) MinSize() fyne.Size { + return r.text.MinSize().Max(fyne.NewSize(48, 48)) +} + +func (r *cellRenderer) Objects() []fyne.CanvasObject { + return []fyne.CanvasObject{r.rect, r.text} +} + +func (r *cellRenderer) Refresh() { + if r.cell.Readonly() { + r.rect.FillColor = theme.ShadowColor() + r.rect.StrokeColor = theme.HoverColor() + } + + if r.cell.Given != "" { + // cell is known to be correct and cannot be modified + r.text.Text = r.cell.Given + } else { + // cell is unknown and can be selected and modified + if r.cell.hovered { + r.rect.FillColor = theme.HoverColor() + } else if r.cell.selected { + r.rect.FillColor = theme.FocusColor() + } else { + r.rect.FillColor = color.Transparent + } + + if r.cell.mistake { + r.rect.StrokeColor = theme.PrimaryColorNamed(theme.ColorRed) + } else if r.cell.selected { + r.rect.StrokeColor = theme.FocusColor() + } else { + r.rect.StrokeColor = theme.ShadowColor() + } + + r.text.Text = r.cell.Center + } + + r.rect.Refresh() + r.text.Refresh() +} diff --git a/gen.go b/cmd/gen.go similarity index 96% rename from gen.go rename to cmd/gen.go index bdb8856..f5b8d16 100644 --- a/gen.go +++ b/cmd/gen.go @@ -38,7 +38,7 @@ func getCommitSHA() string { func getPackageName() string { _, b, _, _ := runtime.Caller(0) - return filepath.Base(filepath.Dir(b)) + return filepath.Base(filepath.Dir(filepath.Join(b, ".."))) } func getVersion() string { diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..b255f40 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +//go:generate go run gen.go + +// TODO: These become configuration + +// HighlightMistakes will become a configuration variable representing whether +// summary mistakes are checked for with every set. +var HighlightMistakes = true + +var wikipedia = `3,3,3,3,53--7----6--195----98----6-8---6---34--8-3--17---2---6-6----28----419--5----8--79` + +func main() { + a := app.NewWithID("com.github.albinogeek.number-place") + b, _ := start(a) + b.load(wikipedia) + b.gameTimer.Show() + b.gameTimer.Refresh() + a.Run() +} + +func start(a fyne.App) (*board, fyne.Window) { + w := a.NewWindow("Number Place") + b := newBoard(3, 3, 3, 3) + + uiInit(b, w) + + w.Show() + + return b, w +} + +func uiInit(b *board, w fyne.Window) { + controls := make([]fyne.CanvasObject, 0) + + values := make(map[string]bool) + // TODO: Support other digit systems (such as HEX for sandwiche or Giant) + for i := 0; i < b.cellsPerRow; i++ { + v := strconv.Itoa(1 + i) + controls = append(controls, widget.NewButton(v, setSelected(b, v))) + values[v] = true + } + + w.Canvas().SetOnTypedKey(keyHandler(b, values)) + + controlArea := container.NewPadded(container.NewVBox( + fyne.NewContainerWithLayout( + layout.NewGridLayout(b.boxWidth), + controls..., + ), + widget.NewSeparator(), + fyne.NewContainerWithLayout( + layout.NewGridLayout(3), + widget.NewButtonWithIcon("", theme.CancelIcon(), clearSelected(b)), + widget.NewButtonWithIcon("", theme.ContentUndoIcon(), b.Undo), + widget.NewButtonWithIcon("", theme.ConfirmIcon(), func() { + if err := b.check(); err != nil { + dialog.ShowError(err, w) + return + } + + dialog.ShowInformation("Check Passed", + "I don't see any mistakes right now.\nIt's up to you to complete the puzzle.", w) + }), + ), + widget.NewSeparator(), + widget.NewButtonWithIcon("", theme.DeleteIcon(), b.Reset), + layout.NewSpacer(), + container.NewHBox(b.gameTimer), + )) + + w.SetContent(container.NewBorder( + nil, nil, nil, + + // Right + controlArea, + + // Objects + b.Container, + )) + + w.SetFixedSize(true) + w.CenterOnScreen() +} + +func keyHandler(b *board, values map[string]bool) func(*fyne.KeyEvent) { + key := func(value string) { + if yes, ok := values[value]; !ok || !yes { + return + } + + setSelected(b, value)() + } + + return func(ke *fyne.KeyEvent) { + switch ke.Name { + case fyne.Key1: + key("1") + case fyne.Key2: + key("2") + case fyne.Key3: + key("3") + case fyne.Key4: + key("4") + case fyne.Key5: + key("5") + case fyne.Key6: + key("6") + case fyne.Key7: + key("7") + case fyne.Key8: + key("8") + case fyne.Key9: + key("9") + case fyne.KeyDelete: + clearSelected(b)() + case fyne.KeyBackspace: + clearSelected(b)() + } + } +} + +func clearSelected(b *board) func() { + return func() { + setSelected(b, "")() + } +} + +func setSelected(b *board, value string) func() { + return func() { + b.registerUndo() + + b.mu.Lock() + for _, c := range b.cells { + if c.selected { + c.selected = false + c.SetCenter(value) + } else { + c.SetMistake(false) + } + } + b.mu.Unlock() + + b.check() + } +} diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..4487e9d --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "strconv" + "testing" + + "fyne.io/fyne/v2/test" + + "github.com/stretchr/testify/assert" +) + +// This should be a perfectly valid (but not solvable) grid that looks like: +// +// ABC DEF +// GHI JKL +// MNO PQR +var testBoardKey = `3,3,2,1,ABCDEFGHIJKLMNOPQR` + +func TestUIBoardKey(t *testing.T) { + var ( + a = test.NewApp() + board, w = start(a) + ) + a.Run() + + test.AssertImageMatches(t, "start-empty.png", w.Canvas().Capture()) + + assert.NoError(t, board.load(testBoardKey), "failed loading test board key") + + v := board.Container + _ = v + + test.AssertImageMatches(t, "board-key.png", w.Canvas().Capture()) +} + +func TestUICells(t *testing.T) { + var ( + a = test.NewApp() + board, w = start(a) + ) + a.Run() + + test.AssertImageMatches(t, "start-empty.png", w.Canvas().Capture()) + + assert.NoError(t, board.load(wikipedia), "failed loading valid classic sudoku") + + test.AssertImageMatches(t, "start.png", w.Canvas().Capture()) + + // Test all given cells + c := board.cells[0] + w.SetContent(c) + for i := 1; i < 10; i++ { + c.SetGiven(strconv.Itoa(i)) + c.Refresh() + + test.AssertImageMatches(t, fmt.Sprintf("cell-given-%d.png", i), w.Canvas().Capture()) + } + + // Test empty cells + c = board.cells[2] + w.SetContent(c) + c.Refresh() + + test.AssertImageMatches(t, "cell-empty.png", w.Canvas().Capture()) + + // Test center-valued cells + for i := 1; i < 10; i++ { + c.SetCenter(strconv.Itoa(i)) + c.Refresh() + + test.AssertImageMatches(t, fmt.Sprintf("cell-center-%d.png", i), w.Canvas().Capture()) + } +} + +func TestUICellSelect(t *testing.T) { + var ( + a = test.NewApp() + board, w = start(a) + + cells = []*cell{ + board.cells[0], + board.cells[13], + board.cells[26], + } + ) + a.Run() + + // test setSelected + for _, c := range cells { + c.Select() + } + + v := "1" + setSelected(board, v)() + test.AssertImageMatches(t, "some-set.png", w.Canvas().Capture()) + + for _, c := range cells { + assert.Equal(t, v, c.Center) + assert.Equal(t, false, c.mistake) + c.Select() + } + + test.AssertImageMatches(t, "some-set-selected.png", w.Canvas().Capture()) + + // test clearSelected + clearSelected(board)() + v = "" + for _, c := range cells { + assert.Equal(t, v, c.Center) + } +} diff --git a/cmd/testdata/board-get-box-0.png b/cmd/testdata/board-get-box-0.png new file mode 100644 index 0000000..6e132c0 Binary files /dev/null and b/cmd/testdata/board-get-box-0.png differ diff --git a/cmd/testdata/board-get-box-1.png b/cmd/testdata/board-get-box-1.png new file mode 100644 index 0000000..77671d3 Binary files /dev/null and b/cmd/testdata/board-get-box-1.png differ diff --git a/cmd/testdata/board-get-box-2.png b/cmd/testdata/board-get-box-2.png new file mode 100644 index 0000000..3ba60d8 Binary files /dev/null and b/cmd/testdata/board-get-box-2.png differ diff --git a/cmd/testdata/board-get-box-3.png b/cmd/testdata/board-get-box-3.png new file mode 100644 index 0000000..2e80694 Binary files /dev/null and b/cmd/testdata/board-get-box-3.png differ diff --git a/cmd/testdata/board-get-box-4.png b/cmd/testdata/board-get-box-4.png new file mode 100644 index 0000000..e41eca1 Binary files /dev/null and b/cmd/testdata/board-get-box-4.png differ diff --git a/cmd/testdata/board-get-box-5.png b/cmd/testdata/board-get-box-5.png new file mode 100644 index 0000000..7f415f5 Binary files /dev/null and b/cmd/testdata/board-get-box-5.png differ diff --git a/cmd/testdata/board-get-box-6.png b/cmd/testdata/board-get-box-6.png new file mode 100644 index 0000000..10301be Binary files /dev/null and b/cmd/testdata/board-get-box-6.png differ diff --git a/cmd/testdata/board-get-box-7.png b/cmd/testdata/board-get-box-7.png new file mode 100644 index 0000000..d334315 Binary files /dev/null and b/cmd/testdata/board-get-box-7.png differ diff --git a/cmd/testdata/board-get-box-8.png b/cmd/testdata/board-get-box-8.png new file mode 100644 index 0000000..6f0a3fd Binary files /dev/null and b/cmd/testdata/board-get-box-8.png differ diff --git a/cmd/testdata/board-key.png b/cmd/testdata/board-key.png new file mode 100644 index 0000000..fced995 Binary files /dev/null and b/cmd/testdata/board-key.png differ diff --git a/cmd/testdata/cell-center-1.png b/cmd/testdata/cell-center-1.png new file mode 100644 index 0000000..e0efe0a Binary files /dev/null and b/cmd/testdata/cell-center-1.png differ diff --git a/cmd/testdata/cell-center-2.png b/cmd/testdata/cell-center-2.png new file mode 100644 index 0000000..1e19016 Binary files /dev/null and b/cmd/testdata/cell-center-2.png differ diff --git a/cmd/testdata/cell-center-3.png b/cmd/testdata/cell-center-3.png new file mode 100644 index 0000000..eff80c2 Binary files /dev/null and b/cmd/testdata/cell-center-3.png differ diff --git a/cmd/testdata/cell-center-4.png b/cmd/testdata/cell-center-4.png new file mode 100644 index 0000000..c4c6b04 Binary files /dev/null and b/cmd/testdata/cell-center-4.png differ diff --git a/cmd/testdata/cell-center-5.png b/cmd/testdata/cell-center-5.png new file mode 100644 index 0000000..b123fa0 Binary files /dev/null and b/cmd/testdata/cell-center-5.png differ diff --git a/cmd/testdata/cell-center-6.png b/cmd/testdata/cell-center-6.png new file mode 100644 index 0000000..840d2ef Binary files /dev/null and b/cmd/testdata/cell-center-6.png differ diff --git a/cmd/testdata/cell-center-7.png b/cmd/testdata/cell-center-7.png new file mode 100644 index 0000000..717b03a Binary files /dev/null and b/cmd/testdata/cell-center-7.png differ diff --git a/cmd/testdata/cell-center-8.png b/cmd/testdata/cell-center-8.png new file mode 100644 index 0000000..5ffda73 Binary files /dev/null and b/cmd/testdata/cell-center-8.png differ diff --git a/cmd/testdata/cell-center-9.png b/cmd/testdata/cell-center-9.png new file mode 100644 index 0000000..00a575e Binary files /dev/null and b/cmd/testdata/cell-center-9.png differ diff --git a/cmd/testdata/cell-empty.png b/cmd/testdata/cell-empty.png new file mode 100644 index 0000000..6ffe311 Binary files /dev/null and b/cmd/testdata/cell-empty.png differ diff --git a/cmd/testdata/cell-given-1.png b/cmd/testdata/cell-given-1.png new file mode 100644 index 0000000..aacc155 Binary files /dev/null and b/cmd/testdata/cell-given-1.png differ diff --git a/cmd/testdata/cell-given-2.png b/cmd/testdata/cell-given-2.png new file mode 100644 index 0000000..17cd376 Binary files /dev/null and b/cmd/testdata/cell-given-2.png differ diff --git a/cmd/testdata/cell-given-3.png b/cmd/testdata/cell-given-3.png new file mode 100644 index 0000000..4443cc8 Binary files /dev/null and b/cmd/testdata/cell-given-3.png differ diff --git a/cmd/testdata/cell-given-4.png b/cmd/testdata/cell-given-4.png new file mode 100644 index 0000000..c078883 Binary files /dev/null and b/cmd/testdata/cell-given-4.png differ diff --git a/cmd/testdata/cell-given-5.png b/cmd/testdata/cell-given-5.png new file mode 100644 index 0000000..03f6f5d Binary files /dev/null and b/cmd/testdata/cell-given-5.png differ diff --git a/cmd/testdata/cell-given-6.png b/cmd/testdata/cell-given-6.png new file mode 100644 index 0000000..52bfce9 Binary files /dev/null and b/cmd/testdata/cell-given-6.png differ diff --git a/cmd/testdata/cell-given-7.png b/cmd/testdata/cell-given-7.png new file mode 100644 index 0000000..e91d0a9 Binary files /dev/null and b/cmd/testdata/cell-given-7.png differ diff --git a/cmd/testdata/cell-given-8.png b/cmd/testdata/cell-given-8.png new file mode 100644 index 0000000..b18ec52 Binary files /dev/null and b/cmd/testdata/cell-given-8.png differ diff --git a/cmd/testdata/cell-given-9.png b/cmd/testdata/cell-given-9.png new file mode 100644 index 0000000..cc155c7 Binary files /dev/null and b/cmd/testdata/cell-given-9.png differ diff --git a/cmd/testdata/some-set-selected.png b/cmd/testdata/some-set-selected.png new file mode 100644 index 0000000..ddf8d86 Binary files /dev/null and b/cmd/testdata/some-set-selected.png differ diff --git a/cmd/testdata/some-set.png b/cmd/testdata/some-set.png new file mode 100644 index 0000000..a5fd6f5 Binary files /dev/null and b/cmd/testdata/some-set.png differ diff --git a/cmd/testdata/start-empty.png b/cmd/testdata/start-empty.png new file mode 100644 index 0000000..1c4d27e Binary files /dev/null and b/cmd/testdata/start-empty.png differ diff --git a/cmd/testdata/start.png b/cmd/testdata/start.png new file mode 100644 index 0000000..75501a7 Binary files /dev/null and b/cmd/testdata/start.png differ diff --git a/version.go b/cmd/version.go similarity index 50% rename from version.go rename to cmd/version.go index 6588b74..a6a4ca7 100644 --- a/version.go +++ b/cmd/version.go @@ -1,10 +1,10 @@ // Code generated by go generate; DO NOT EDIT. -// This file was generated at: 2021-01-12 19:16:04.70194496 -0800 PST m=+0.000447316 -// This file is in sync with: a047f05cbf81d581f56b7d3e59e7e71e3c9e538d +// This file was generated at: 2021-01-20 07:00:41.588471632 -0800 PST m=+0.000893210 +// This file is in sync with: unknown-commit package main // PROGRAM is the human readable product name const PROGRAM = "number-place" // VERSION is the human readable product version -const VERSION = "7102932" +const VERSION = "v0.1-80-g1b25237" diff --git a/go.mod b/go.mod index 2616423..199914b 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,17 @@ module github.com/AlbinoGeek/number-place go 1.15 require ( - fyne.io/fyne v1.4.4-0.20210112232308-ee882d721766 + fyne.io/fyne/v2 v2.0.0 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20201108214237-06ea97f0c265 // indirect + github.com/json-iterator/go v1.1.10 + github.com/kataras/golog v0.1.6 github.com/kr/text v0.2.0 // indirect - github.com/stretchr/testify v1.6.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/stretchr/testify v1.7.0 golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect - golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect - golang.org/x/sys v0.0.0-20210113000019-eaf3bda374d2 // indirect + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect + golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/text v0.3.5 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index f205c6f..4b99c6b 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ -fyne.io/fyne v1.4.4-0.20210112232308-ee882d721766 h1:fID26qzMSCDwFJCESH57Rn/XGXI5dxsU96cSw7qjM6I= -fyne.io/fyne v1.4.4-0.20210112232308-ee882d721766/go.mod h1:8kiPBNSDmuplxs9WnKCkaWYqbcXFy0DeAzwa6PBO9Z8= +fyne.io/fyne/v2 v2.0.0 h1:TfsS3bNq5663BpXsoz1OfzyjcaMqqOf9usI8ZKkw4IE= +fyne.io/fyne/v2 v2.0.0/go.mod h1:FmobqvPpBW+nG1nDyxZWf1SQLED9g/vXIxiIIVjHazY= github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fyne-io/mobile v0.1.2 h1:0HaXDtOOwyOTn3Umi0uKVCOgJtfX73c6unC4U8i5VZU= @@ -20,8 +22,15 @@ github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kataras/golog v0.1.6 h1:jEqEQCm+4B4M4/CgzrEWNj9iUST0hGZXDqAPMVnxWMw= +github.com/kataras/golog v0.1.6/go.mod h1:jOSQ+C5fUqsNSwurB/oAHq1IFSb0KI3l6GMa7xB6dZA= +github.com/kataras/pio v0.0.10 h1:b0qtPUqOpM2O+bqa5wr2O6dN4cQNwSmFd6HQqgVae0g= +github.com/kataras/pio v0.0.10/go.mod h1:gS3ui9xSD+lAUpbYnjOGiQyY7sUMJO+EHpiRzhtZ5no= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -30,6 +39,15 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -45,10 +63,12 @@ github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCE github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -57,14 +77,16 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnM golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -74,9 +96,10 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 h1:gVCS+QOncANNPlmlO1AhlU3oxs4V9z+gTtPwIk3p2N8= golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210113000019-eaf3bda374d2 h1:F9vNgpIiamoF+Q1/c78bikg/NScXEtbZSNEpnRelOzs= -golang.org/x/sys v0.0.0-20210113000019-eaf3bda374d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -86,9 +109,11 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03 h1:XpToik3MpT5iW3iHgNwnh3a8QwugfomvxOlyDnaOils= golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/main.go b/main.go deleted file mode 100644 index c54f985..0000000 --- a/main.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fyne.io/fyne" - "fyne.io/fyne/app" -) - -//go:generate go run gen.go - -func main() { - var ( - a = app.NewWithID("com.github.albinogeek.number-place") - w = a.NewWindow("Number Place") - ) - - uiInit(w) - - w.Show() - a.Run() -} - -func uiInit(w fyne.Window) { - w.CenterOnScreen() -}