Starting on UI. Coverage no longer 100% :c
This commit is contained in:
107
state/history.go
107
state/history.go
@ -1,20 +1,27 @@
|
||||
package state
|
||||
|
||||
import "strconv"
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// History returns the gameplay history for replays
|
||||
func (s *State) History() string {
|
||||
return s.history
|
||||
}
|
||||
|
||||
func UnmarshalAll(history string) *State {
|
||||
func UnmarshalAll(history string) (*State, error) {
|
||||
s := Unmarshal(history)
|
||||
for {
|
||||
if !s.Step() {
|
||||
res, err := s.Step()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !res {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func Unmarshal(history string) *State {
|
||||
@ -25,16 +32,33 @@ func Unmarshal(history string) *State {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) Step() bool {
|
||||
func (s *State) Step() (bool, error) {
|
||||
if s.historyIndex >= len(s.history) {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch s.history[s.historyIndex] {
|
||||
case 'i', 'm', 'f', 'c', 'r', 'l', 'u', 'd', 'R', 'L', 'U', 'D':
|
||||
err := s.beforeAct()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
switch s.history[s.historyIndex] {
|
||||
case 'g':
|
||||
s.historyStart()
|
||||
if s.initialized {
|
||||
return false, fmt.Errorf("initialization step in invalid location (index %d)", s.historyIndex)
|
||||
}
|
||||
err := s.historyStart()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
case 'i':
|
||||
s.historyInitSection()
|
||||
err := s.historyInitSection()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
case 'm':
|
||||
s.mark(*s.cursor)
|
||||
case 'f':
|
||||
@ -57,15 +81,43 @@ func (s *State) Step() bool {
|
||||
s.cursorSectionUp()
|
||||
case 'D':
|
||||
s.cursorSectionDown()
|
||||
case 't': // TODO for now, this is just sugar. Will be a timestamp for events completed.
|
||||
for {
|
||||
s.historyIndex++
|
||||
if s.historyIndex >= len(s.history) || s.history[s.historyIndex] == ')' {
|
||||
break
|
||||
}
|
||||
}
|
||||
case ' ', '\n', '\t':
|
||||
break
|
||||
case '#':
|
||||
for {
|
||||
s.historyIndex++
|
||||
if s.historyIndex >= len(s.history) || s.history[s.historyIndex] == '\n' {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("invalid step in history: %s (index %d)", string(s.history[s.historyIndex]), s.historyIndex)
|
||||
}
|
||||
s.historyIndex++
|
||||
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *State) historyStart() {
|
||||
func (s *State) beforeAct() error {
|
||||
if !s.initialized {
|
||||
return fmt.Errorf("tried to act on an uninitialized state (index %d)", s.historyIndex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) historyStart() error {
|
||||
peek := s.historyIndex + 1
|
||||
p, peek := s.historyPoint(peek)
|
||||
p, peek, err := s.historyPoint(peek)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.sectionSize = p.X
|
||||
s.cellsPerSection = p.Y
|
||||
s.cells = newField(s.size())
|
||||
@ -75,11 +127,17 @@ func (s *State) historyStart() {
|
||||
s.colHeaders = make([]header, s.size())
|
||||
s.historyIndex = peek
|
||||
s.score.Blackout = make([]bool, s.size())
|
||||
s.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) historyInitSection() {
|
||||
func (s *State) historyInitSection() error {
|
||||
peek := s.historyIndex + 1
|
||||
p, peek := s.historyPoint(peek)
|
||||
p, peek, err := s.historyPoint(peek)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
segment := []byte(s.history[peek : peek+s.cellsPerSection*s.cellsPerSection])
|
||||
for i, c := range segment {
|
||||
curr := Point{
|
||||
@ -98,36 +156,49 @@ func (s *State) historyInitSection() {
|
||||
peek++
|
||||
}
|
||||
s.historyIndex = peek
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) historyPoint(index int) (Point, int) {
|
||||
func (s *State) historyPoint(index int) (Point, int, error) {
|
||||
var x, y string
|
||||
|
||||
// Advance past paren
|
||||
// Advance past opening paren
|
||||
index++
|
||||
for {
|
||||
if index >= len(s.history) {
|
||||
return Point{}, 0, fmt.Errorf("point.X never ended? (index %d)", index)
|
||||
}
|
||||
switch s.history[index] {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
x += string(s.history[index])
|
||||
index++
|
||||
continue
|
||||
case ',':
|
||||
index++
|
||||
default:
|
||||
return Point{}, 0, fmt.Errorf("invalid character in point.X: %s (index %d)", string(s.history[index]), index)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Advance past comma
|
||||
index++
|
||||
for {
|
||||
if index >= len(s.history) {
|
||||
return Point{}, 0, fmt.Errorf("point.Y never ended? (index %d)", index)
|
||||
}
|
||||
switch s.history[index] {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
y += string(s.history[index])
|
||||
index++
|
||||
continue
|
||||
case ')':
|
||||
break
|
||||
default:
|
||||
return Point{}, 0, fmt.Errorf("invalid character in point.Y: %s (index %d)", string(s.history[index]), index)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
pX, _ := strconv.Atoi(x)
|
||||
pY, _ := strconv.Atoi(y)
|
||||
return Point{pX, pY}, index
|
||||
return Point{pX, pY}, index, nil
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
@ -31,8 +32,9 @@ func TestHistory(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("You can load a game from its saved history", func() {
|
||||
history := "g(2,2)i(0,0)oxoxi(1,0)xoxoi(0,1)ooooi(1,1)xxxxfRmDcLUrldu"
|
||||
s := UnmarshalAll(history)
|
||||
history := "g(2,2)i(0,0)oxoxi(1,0)xoxoi(0,1)ooooi(1,1)xxxx\n# Here we goooo~\nfRmDcLUrldut(1773881959)"
|
||||
s, err := UnmarshalAll(history)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("It sets the history", func() {
|
||||
So(s.History(), ShouldEqual, history)
|
||||
@ -53,5 +55,44 @@ func TestHistory(t *testing.T) {
|
||||
So(s.String(), ShouldEqual, "X. \n \no .\n....\n")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Errors are handled during loading", func() {
|
||||
Convey("It raises errors if acting on an uninitialized state", func() {
|
||||
s, err := UnmarshalAll("m")
|
||||
So(err, ShouldResemble, errors.New("tried to act on an uninitialized state (index 0)"))
|
||||
So(s, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("It raises errors if trying to initialize an initialized state", func() {
|
||||
s, err := UnmarshalAll("g(1,1)g(1,1)")
|
||||
So(err, ShouldResemble, errors.New("initialization step in invalid location (index 6)"))
|
||||
So(s, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("It raises errors in reading points", func() {
|
||||
s, err := UnmarshalAll("g(1")
|
||||
So(err, ShouldResemble, errors.New("point.X never ended? (index 3)"))
|
||||
So(s, ShouldBeNil)
|
||||
|
||||
s, err = UnmarshalAll("g(a,1)")
|
||||
So(err, ShouldResemble, errors.New("invalid character in point.X: a (index 2)"))
|
||||
So(s, ShouldBeNil)
|
||||
|
||||
s, err = UnmarshalAll("g(1,1)i(0,1")
|
||||
So(err, ShouldResemble, errors.New("point.Y never ended? (index 11)"))
|
||||
So(s, ShouldBeNil)
|
||||
|
||||
s, err = UnmarshalAll("g(1,a)")
|
||||
So(err, ShouldResemble, errors.New("invalid character in point.Y: a (index 4)"))
|
||||
So(s, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("It only accepts valid steps", func() {
|
||||
history := "g(2,2)i(0,0)oxoxi(1,0)xoxoi(0,1)ooooi(1,1)xxxxz"
|
||||
s, err := UnmarshalAll(history)
|
||||
So(err, ShouldResemble, errors.New("invalid step in history: z (index 46)"))
|
||||
So(s, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Point struct {
|
||||
@ -25,6 +26,8 @@ type header struct {
|
||||
}
|
||||
|
||||
type State struct {
|
||||
initialized bool
|
||||
|
||||
cursor *Point
|
||||
|
||||
score score
|
||||
@ -56,6 +59,7 @@ func New(sectionSize, cellsPerSection int) *State {
|
||||
}
|
||||
}
|
||||
// TODO reveal 1 row per section, 1 col per sections/2
|
||||
s.initialized = true
|
||||
return s
|
||||
}
|
||||
|
||||
@ -259,6 +263,7 @@ func (s *State) update(p Point) {
|
||||
s.sections.setCorrect(sectionPoint, true)
|
||||
s.updateCompletedSections()
|
||||
s.scoreValidCompletedSections()
|
||||
s.history += fmt.Sprintf("t(%d)", time.Now().Unix())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
@ -62,10 +64,13 @@ func TestState(t *testing.T) {
|
||||
s.cells.vivify(Point{2, 2})
|
||||
|
||||
Convey("It marks sections as correct when they are correctly guessed", func() {
|
||||
s.mark(Point{0, 0})
|
||||
s.Mark()
|
||||
So(s.cells.correct(Point{0, 0}), ShouldBeTrue)
|
||||
So(s.sections.correct(Point{0, 0}), ShouldBeTrue)
|
||||
So(s.String(), ShouldEqual, "O . \n \n. . \n \n")
|
||||
Convey("It adds a timestamp to the history when a section is completed", func() {
|
||||
So(s.history, ShouldEndWith, fmt.Sprintf("mt(%d)", time.Now().Unix()))
|
||||
})
|
||||
})
|
||||
|
||||
Convey("It marks sections as complete if they meet the criteria", func() {
|
||||
@ -84,7 +89,6 @@ func TestState(t *testing.T) {
|
||||
s.mark(Point{2, 0})
|
||||
s.mark(Point{0, 2})
|
||||
s.mark(Point{2, 2})
|
||||
//So(res, ShouldResemble, []bool{true, true, true, true})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
95
state/view.go
Normal file
95
state/view.go
Normal file
@ -0,0 +1,95 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
complete = []string{"██ ", "██ "}
|
||||
flagged = []string{"╲╱ ", "╱╲ "}
|
||||
marked = []string{"╭╮ ", "╰╯ "}
|
||||
blank = []string{".- ", " "}
|
||||
|
||||
cursorStyle = lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("#005566"))
|
||||
|
||||
gridStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Black)
|
||||
)
|
||||
|
||||
func (c *cell) View() []string {
|
||||
if c.complete() {
|
||||
return complete
|
||||
} else if c.flagged() {
|
||||
return flagged
|
||||
} else if c.marked() {
|
||||
return marked
|
||||
} else {
|
||||
return blank
|
||||
}
|
||||
}
|
||||
|
||||
func (f *field) View(p *Point, sectionSize, cellsPerSection int) string {
|
||||
res := gridStyle.Render("┌")
|
||||
for c := 0; c < cellsPerSection*sectionSize*3; c++ {
|
||||
res += gridStyle.Render("─")
|
||||
if c == cellsPerSection*sectionSize*3-1 {
|
||||
res += gridStyle.Render("┐") + "\n"
|
||||
break
|
||||
}
|
||||
if c%(cellsPerSection*3) == cellsPerSection*3-1 {
|
||||
res += gridStyle.Render("┬")
|
||||
}
|
||||
}
|
||||
resA := gridStyle.Render("│")
|
||||
resB := gridStyle.Render("│")
|
||||
for i, c := range f.cells {
|
||||
cellRes := c.View()
|
||||
if i == f.i(*p) {
|
||||
resA += cursorStyle.Render(cellRes[0])
|
||||
resB += cursorStyle.Render(cellRes[1])
|
||||
} else {
|
||||
resA += cellRes[0]
|
||||
resB += cellRes[1]
|
||||
}
|
||||
if i%cellsPerSection == cellsPerSection-1 {
|
||||
resA += gridStyle.Render("│")
|
||||
resB += gridStyle.Render("│")
|
||||
}
|
||||
if i%f.size == f.size-1 {
|
||||
res += fmt.Sprintf("%s\n%s\n", resA, resB)
|
||||
resA = gridStyle.Render("│")
|
||||
resB = gridStyle.Render("│")
|
||||
}
|
||||
if i != 0 && i%(cellsPerSection*cellsPerSection*sectionSize) == 0 {
|
||||
res += gridStyle.Render("├")
|
||||
for section := 0; section < sectionSize; section++ {
|
||||
for sectionCell := 0; sectionCell < cellsPerSection; sectionCell++ {
|
||||
res += gridStyle.Render("───")
|
||||
}
|
||||
if section < sectionSize-1 {
|
||||
res += gridStyle.Render("┼")
|
||||
}
|
||||
}
|
||||
res += gridStyle.Render("┤") + "\n"
|
||||
}
|
||||
}
|
||||
res += gridStyle.Render("└")
|
||||
for c := 0; c < cellsPerSection*sectionSize*3; c++ {
|
||||
res += gridStyle.Render("─")
|
||||
if c == cellsPerSection*sectionSize*3-1 {
|
||||
res += gridStyle.Render("┘\n")
|
||||
break
|
||||
}
|
||||
if c%(cellsPerSection*3) == cellsPerSection*3-1 {
|
||||
res += gridStyle.Render("┴")
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *State) View() string {
|
||||
return s.cells.View(s.cursor, s.sectionSize, s.cellsPerSection)
|
||||
}
|
||||
Reference in New Issue
Block a user