From 29bd181e4fb231b8aa830e8d810b655b09b77730 Mon Sep 17 00:00:00 2001 From: Madison Rye Progress Date: Tue, 26 Aug 2025 22:26:08 -0700 Subject: [PATCH] Reorganize state --- model/cursor.go | 49 ------------ model/cursor_test.go | 107 -------------------------- model/model.go | 12 ++- model/tea.go | 22 +++--- model/update.go | 17 ----- state/cell.go | 104 ++++++++++++++++++++++++++ state/cell_test.go | 101 +++++++++++++++++++++++++ state/cursor.go | 49 ++++++++++++ state/cursor_test.go | 107 ++++++++++++++++++++++++++ state/field.go | 99 ++++++++++++++++++++++++ state/field_test.go | 54 ++++++++++++++ state/state.go | 174 +++++++++++++++++++++++++++++++++++++++++++ state/state_test.go | 43 +++++++++++ 13 files changed, 747 insertions(+), 191 deletions(-) delete mode 100644 model/cursor.go delete mode 100644 model/cursor_test.go delete mode 100644 model/update.go create mode 100644 state/cell.go create mode 100644 state/cell_test.go create mode 100644 state/cursor.go create mode 100644 state/cursor_test.go create mode 100644 state/field.go create mode 100644 state/field_test.go create mode 100644 state/state.go create mode 100644 state/state_test.go diff --git a/model/cursor.go b/model/cursor.go deleted file mode 100644 index 3248e8b..0000000 --- a/model/cursor.go +++ /dev/null @@ -1,49 +0,0 @@ -package model - -func (m model) cursorCellUp() { - if m.cursor.y >= 1 { - m.cursor.y-- - } -} - -func (m model) cursorCellDown() { - if m.cursor.y < m.fieldSize-1 { - m.cursor.y++ - } -} - -func (m model) cursorCellRight() { - if m.cursor.x < m.fieldSize-1 { - m.cursor.x++ - } -} - -func (m model) cursorCellLeft() { - if m.cursor.x >= 1 { - m.cursor.x-- - } -} - -func (m model) cursorSectionUp() { - if m.cursor.y >= m.cellsPerSection { - m.cursor.y -= m.cellsPerSection - } -} - -func (m model) cursorSectionDown() { - if m.cursor.y < m.fieldSize-m.cellsPerSection { - m.cursor.y += m.cellsPerSection - } -} - -func (m model) cursorSectionRight() { - if m.cursor.x < m.fieldSize-m.cellsPerSection { - m.cursor.x += m.cellsPerSection - } -} - -func (m model) cursorSectionLeft() { - if m.cursor.x >= m.cellsPerSection { - m.cursor.x -= m.cellsPerSection - } -} diff --git a/model/cursor_test.go b/model/cursor_test.go deleted file mode 100644 index b5df1d0..0000000 --- a/model/cursor_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package model - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestCursor(t *testing.T) { - Convey("Given a cursor", t, func() { - m := New(4, 4) - So(*m.cursor, ShouldResemble, point{0, 0}) - - Convey("When moving cell to cell", func() { - - Convey("You can move down", func() { - m.cursorCellDown() - So(*m.cursor, ShouldResemble, point{0, 1}) - }) - - Convey("You can move up", func() { - m.cursorCellDown() - m.cursorCellUp() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can move right", func() { - m.cursorCellRight() - So(*m.cursor, ShouldResemble, point{1, 0}) - }) - - Convey("You can move left", func() { - m.cursorCellRight() - m.cursorCellLeft() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can't move up beyond the top", func() { - m.cursorCellUp() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can't move left beyond the edge", func() { - m.cursorCellLeft() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can't move down below the bottom", func() { - m.cursor = &point{15, 15} - m.cursorCellDown() - So(*m.cursor, ShouldResemble, point{15, 15}) - }) - - Convey("You can't move right beyond the edge", func() { - m.cursor = &point{15, 15} - m.cursorCellRight() - So(*m.cursor, ShouldResemble, point{15, 15}) - }) - }) - - Convey("When moving section to section", func() { - Convey("You can move down", func() { - m.cursorSectionDown() - So(*m.cursor, ShouldResemble, point{0, 4}) - }) - - Convey("You can move up", func() { - m.cursorSectionDown() - m.cursorSectionUp() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can move right", func() { - m.cursorSectionRight() - So(*m.cursor, ShouldResemble, point{4, 0}) - }) - - Convey("You can move left", func() { - m.cursorSectionRight() - m.cursorSectionLeft() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can't move up beyond the top", func() { - m.cursorCellUp() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can't move left beyond the edge", func() { - m.cursorCellLeft() - So(*m.cursor, ShouldResemble, point{0, 0}) - }) - - Convey("You can't move down below the bottom", func() { - m.cursor = &point{15, 15} - m.cursorSectionDown() - So(*m.cursor, ShouldResemble, point{15, 15}) - }) - - Convey("You can't move right beyond the edge", func() { - m.cursor = &point{15, 15} - m.cursorSectionRight() - So(*m.cursor, ShouldResemble, point{15, 15}) - }) - }) - }) -} diff --git a/model/model.go b/model/model.go index 5135493..39eb0f3 100644 --- a/model/model.go +++ b/model/model.go @@ -1,17 +1,15 @@ package model -type point struct { - x, y int -} +import "git.makyo.dev/makyo/gogogogogram/state" type model struct { fieldSize, sectionSize, cellsPerSection int - state *state + state *state.State clears, score, factor, track int - cursor *point + cursor *state.Point columnStates, rowStates [][]int columnsCorrect, rowsCorrect []bool @@ -24,9 +22,9 @@ func New(sectionSize, cellsPerSection int) model { fieldSize: sectionSize * cellsPerSection, sectionSize: sectionSize, cellsPerSection: cellsPerSection, - cursor: &point{0, 0}, + cursor: &state.Point{0, 0}, } - m.state = newState(sectionSize, cellsPerSection) + m.state = state.New(sectionSize, cellsPerSection) return m } diff --git a/model/tea.go b/model/tea.go index be3a960..6a1bd2b 100644 --- a/model/tea.go +++ b/model/tea.go @@ -19,39 +19,39 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Movement by cell case "up", "w": - m.cursorCellUp() + m.state.CursorCellUp() case "down", "s": - m.cursorCellDown() + m.state.CursorCellDown() case "right", "d": - m.cursorCellRight() + m.state.CursorCellRight() case "left", "a": - m.cursorCellLeft() + m.state.CursorCellLeft() // Movement by section case "ctrl+up", "ctrl+w", "shift+up", "shift+w": - m.cursorSectionUp() + m.state.CursorSectionUp() case "ctrl+down", "ctrl+s", "shift+down", "shift+s": - m.cursorSectionDown() + m.state.CursorSectionDown() case "ctrl+right", "ctrl+d", "shift+right", "shift+d": - m.cursorSectionRight() + m.state.CursorSectionRight() case "ctrl+left", "ctrl+a", "shift+left", "shift+a": - m.cursorSectionRight() + m.state.CursorSectionRight() // Marking/flagging case " ", "enter": - m.state.mark(*m.cursor) + m.state.Mark() case "x": - m.state.flag(*m.cursor) + m.state.Flag() case "delete", "backspace": - m.state.clear(*m.cursor) + m.state.Clear() } } diff --git a/model/update.go b/model/update.go deleted file mode 100644 index ea3eaa4..0000000 --- a/model/update.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -func (m model) update() model { - // Update correctness/sections - - // Check for complete sections, which are those where the row and column are both correct - - // Check for clears, which are at least 2x2 - - // Clear and bump scores - - // Check for blackout - - // Update row/column states/correctness - - return m -} diff --git a/state/cell.go b/state/cell.go new file mode 100644 index 0000000..e59c269 --- /dev/null +++ b/state/cell.go @@ -0,0 +1,104 @@ +package state + +const ( + statebit = 1 // the current state of the cell (for field) + flagbit = statebit << 1 // whether or not the cell has been flagged as empty (for field) + markbit = statebit << 2 // whether or not the cell has been flagged as full (for field) + correctbit = statebit << 3 // whether or not the cell is correct (for field, section) + completebit = statebit << 4 // whether or not the cell is complete (for section) +) + +// Cells {{{ + +// cell represents a single entry in the field managing various states. +type cell byte + +// state returns whether the cell is alive or dead. +func (c cell) state() bool { + return (c & statebit) != 0 +} + +// correct returns whether or not the cell has been guessed correctly, or the section is made up of entirely correct guesses. +func (c cell) correct() bool { + return (c & correctbit) != 0 +} + +// complete marks the section as complete (that is, all sections in its row/column are correct. +func (c cell) complete() bool { + return (c & completebit) != 0 +} + +// flagged returns whether or not the cell is suspected to be dead. +func (c cell) flagged() bool { + return (c & flagbit) != 0 +} + +// marked returns whether or not the cell is suspected to be alive. +func (c cell) marked() bool { + return (c & markbit) != 0 +} + +// vivify sets the state of the cell to alive. +func (c cell) vivify() cell { + c = c | statebit + if c.marked() { + return c.setCorrect(true) + } else { + return c.setCorrect(false) + } +} + +// kill sets the state of the cell to dead. +func (c cell) kill() cell { + c = c &^ statebit + if c.flagged() { + return c.setCorrect(true) + } else { + return c.setCorrect(false) + } +} + +// setCorrect marks the cell as being guessed correctly, or the section being made up of entirely correct guesses. +func (c cell) setCorrect(to bool) cell { + if to { + return c | correctbit + } else { + return c &^ correctbit + } +} + +// setComplete marks the section as complete (that is, all sections in its row/column are correct). +func (c cell) setComplete(to bool) cell { + if to { + return c | completebit + } else { + return c &^ completebit + } +} + +// mark marks a bit as suspected alive. +func (c cell) mark() cell { + c |= markbit + c = c &^ flagbit + if c.state() { + return c.setCorrect(true) + } else { + return c.setCorrect(false) + } +} + +// flag marks a bit as suspected dead. +func (c cell) flag() cell { + c |= flagbit + c = c &^ markbit + if c.state() { + return c.setCorrect(false) + } else { + return c.setCorrect(true) + } +} + +// clear clears all bits except for the status. +func (c cell) clear() cell { + return c &^ (markbit | flagbit | correctbit | completebit) +} diff --git a/state/cell_test.go b/state/cell_test.go new file mode 100644 index 0000000..ae3fb0c --- /dev/null +++ b/state/cell_test.go @@ -0,0 +1,101 @@ +package state + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestCell(t *testing.T) { + Convey("Given a cell", t, func() { + var c cell + + Convey("When managing state", func() { + So(c.state(), ShouldBeFalse) + + Convey("You can vivify and kill the cell", func() { + So(c.state(), ShouldBeFalse) + c = c.vivify() + So(c.state(), ShouldBeTrue) + c = c.kill() + So(c.state(), ShouldBeFalse) + }) + }) + + Convey("When managing flags", func() { + + Convey("You can mark a cell as assumed alive", func() { + So(c.marked(), ShouldBeFalse) + c = c.mark() + So(c.marked(), ShouldBeTrue) + }) + + Convey("You can mark a cell as assumed dead", func() { + So(c.flagged(), ShouldBeFalse) + c = c.flag() + So(c.flagged(), ShouldBeTrue) + }) + + Convey("You can mark/unmark a cell as correct", func() { + So(c.correct(), ShouldBeFalse) + c = c.setCorrect(true) + So(c.correct(), ShouldBeTrue) + c = c.setCorrect(false) + So(c.correct(), ShouldBeFalse) + }) + + Convey("You can mark/unmark a cell as complete", func() { + So(c.complete(), ShouldBeFalse) + c = c.setComplete(true) + So(c.complete(), ShouldBeTrue) + c = c.setComplete(false) + So(c.complete(), ShouldBeFalse) + }) + + Convey("Marking a cell unflags it and vice versa", func() { + c = c.mark() + So(c.marked(), ShouldBeTrue) + So(c.flagged(), ShouldBeFalse) + c = c.flag() + So(c.marked(), ShouldBeFalse) + So(c.flagged(), ShouldBeTrue) + c = c.mark() + So(c.marked(), ShouldBeTrue) + So(c.flagged(), ShouldBeFalse) + }) + + Convey("Marking/flagging a cell manages its correctness", func() { + c = c.vivify() + So(c.correct(), ShouldBeFalse) + c = c.mark() + So(c.correct(), ShouldBeTrue) + c = c.kill() + So(c.correct(), ShouldBeFalse) + + c = c.vivify() + c = c.flag() + So(c.correct(), ShouldBeFalse) + c = c.kill() + So(c.correct(), ShouldBeTrue) + }) + + Convey("You can clear all flags (but leave the state)", func() { + c = c.vivify() + c = c.mark() + c = c.setCorrect(true) + c = c.setComplete(true) + So(c.state(), ShouldBeTrue) + So(c.marked(), ShouldBeTrue) + So(c.flagged(), ShouldBeFalse) + So(c.correct(), ShouldBeTrue) + So(c.complete(), ShouldBeTrue) + c = c.clear() + So(c.state(), ShouldBeTrue) + So(c.marked(), ShouldBeFalse) + So(c.flagged(), ShouldBeFalse) + So(c.correct(), ShouldBeFalse) + So(c.complete(), ShouldBeFalse) + }) + }) + }) +} diff --git a/state/cursor.go b/state/cursor.go new file mode 100644 index 0000000..4cabec5 --- /dev/null +++ b/state/cursor.go @@ -0,0 +1,49 @@ +package state + +func (s *State) CursorCellUp() { + if s.cursor.Y >= 1 { + s.cursor.Y-- + } +} + +func (s *State) CursorCellDown() { + if s.cursor.Y < s.size()-1 { + s.cursor.Y++ + } +} + +func (s *State) CursorCellRight() { + if s.cursor.X < s.size()-1 { + s.cursor.X++ + } +} + +func (s *State) CursorCellLeft() { + if s.cursor.X >= 1 { + s.cursor.X-- + } +} + +func (s *State) CursorSectionUp() { + if s.cursor.Y >= s.cellsPerSection { + s.cursor.Y -= s.cellsPerSection + } +} + +func (s *State) CursorSectionDown() { + if s.cursor.Y < s.size()-s.cellsPerSection { + s.cursor.Y += s.cellsPerSection + } +} + +func (s *State) CursorSectionRight() { + if s.cursor.X < s.size()-s.cellsPerSection { + s.cursor.X += s.cellsPerSection + } +} + +func (s *State) CursorSectionLeft() { + if s.cursor.X >= s.cellsPerSection { + s.cursor.X -= s.cellsPerSection + } +} diff --git a/state/cursor_test.go b/state/cursor_test.go new file mode 100644 index 0000000..a73731d --- /dev/null +++ b/state/cursor_test.go @@ -0,0 +1,107 @@ +package state + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestCursor(t *testing.T) { + Convey("Given a cursor", t, func() { + s := New(4, 4) + So(*s.cursor, ShouldResemble, Point{0, 0}) + + Convey("When moving cell to cell", func() { + + Convey("You can move down", func() { + s.CursorCellDown() + So(*s.cursor, ShouldResemble, Point{0, 1}) + }) + + Convey("You can move up", func() { + s.CursorCellDown() + s.CursorCellUp() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can move right", func() { + s.CursorCellRight() + So(*s.cursor, ShouldResemble, Point{1, 0}) + }) + + Convey("You can move left", func() { + s.CursorCellRight() + s.CursorCellLeft() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can't move up beyond the top", func() { + s.CursorCellUp() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can't move left beyond the edge", func() { + s.CursorCellLeft() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can't move down below the bottom", func() { + s.cursor = &Point{15, 15} + s.CursorCellDown() + So(*s.cursor, ShouldResemble, Point{15, 15}) + }) + + Convey("You can't move right beyond the edge", func() { + s.cursor = &Point{15, 15} + s.CursorCellRight() + So(*s.cursor, ShouldResemble, Point{15, 15}) + }) + }) + + Convey("When moving section to section", func() { + Convey("You can move down", func() { + s.CursorSectionDown() + So(*s.cursor, ShouldResemble, Point{0, 4}) + }) + + Convey("You can move up", func() { + s.CursorSectionDown() + s.CursorSectionUp() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can move right", func() { + s.CursorSectionRight() + So(*s.cursor, ShouldResemble, Point{4, 0}) + }) + + Convey("You can move left", func() { + s.CursorSectionRight() + s.CursorSectionLeft() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can't move up beyond the top", func() { + s.CursorCellUp() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can't move left beyond the edge", func() { + s.CursorCellLeft() + So(*s.cursor, ShouldResemble, Point{0, 0}) + }) + + Convey("You can't move down below the bottom", func() { + s.cursor = &Point{15, 15} + s.CursorSectionDown() + So(*s.cursor, ShouldResemble, Point{15, 15}) + }) + + Convey("You can't move right beyond the edge", func() { + s.cursor = &Point{15, 15} + s.CursorSectionRight() + So(*s.cursor, ShouldResemble, Point{15, 15}) + }) + }) + }) +} diff --git a/state/field.go b/state/field.go new file mode 100644 index 0000000..0e2ec76 --- /dev/null +++ b/state/field.go @@ -0,0 +1,99 @@ +package state + +import "math" + +// field represents a square field of cells. +type field struct { + cells []cell + size int +} + +// newField returns a field of cells, all unset. +func newField(size int) *field { + return &field{ + cells: make([]cell, size*size), + size: size, + } +} + +// fieldFromBytes returns a field of cells given a bytearray; it assumes that the bytearray is a square. +func fieldFromBytes(repr []byte) *field { + f := &field{ + cells: make([]cell, len(repr)), + size: int(math.Sqrt(float64(len(repr)))), + } + for i := range repr { + f.cells[i] = cell(repr[i]) + } + return f +} + +// i is a utility function for translating a point to an array index +func (f *field) i(p Point) int { + return p.Y*f.size + p.X +} + +// String returns a string representation of the field. +func (f *field) String() string { + return string(f.cells) +} + +// state returns whether or not the cell is alive. +func (f *field) state(p Point) bool { + return f.cells[f.i(p)].state() +} + +// correct returns whether or not the guess for the cell's state is correct, or whether or not the section is made up of all complete guesses. +func (f *field) correct(p Point) bool { + return f.cells[f.i(p)].correct() +} + +// complete returns whether or not the current section is complete (that is, all sections in its row/column are correct). +func (f *field) complete(p Point) bool { + return f.cells[f.i(p)].complete() +} + +// flagged returns whether or not the cell is suspected of being dead. +func (f *field) flagged(p Point) bool { + return f.cells[f.i(p)].flagged() +} + +// marked returns whether or not the cell is suspected of being alive. +func (f *field) marked(p Point) bool { + return f.cells[f.i(p)].marked() +} + +// vivify sets the cell to alive. +func (f *field) vivify(p Point) { + f.cells[f.i(p)] = f.cells[f.i(p)].vivify() +} + +// kill sets the cell to dead. +func (f *field) kill(p Point) { + f.cells[f.i(p)] = f.cells[f.i(p)].kill() +} + +// setCorrect sets the whether or not the guess at the cell's state is correct +func (f *field) setCorrect(p Point, to bool) { + f.cells[f.i(p)] = f.cells[f.i(p)].setCorrect(to) +} + +// setComplete sets whether or not the section is complete (that is, all of its rows/columns are correct). +func (f *field) setComplete(p Point, to bool) { + f.cells[f.i(p)] = f.cells[f.i(p)].setComplete(to) +} + +// mark marks the cell as suspected alive. +func (f *field) mark(p Point) { + f.cells[f.i(p)] = f.cells[f.i(p)].mark() +} + +// flag marks the cell as suspected dead. +func (f *field) flag(p Point) { + f.cells[f.i(p)] = f.cells[f.i(p)].flag() +} + +// clear clears all but the cell's state. +func (f *field) clear(p Point) { + f.cells[f.i(p)] = f.cells[f.i(p)].clear() +} diff --git a/state/field_test.go b/state/field_test.go new file mode 100644 index 0000000..53e9cfc --- /dev/null +++ b/state/field_test.go @@ -0,0 +1,54 @@ +package state + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestField(t *testing.T) { + Convey("Given a field of cells", t, func() { + p := Point{0, 0} + f := newField(2) + So(f.size, ShouldEqual, 2) + So(len(f.cells), ShouldEqual, 4) + + Convey("You can load from a bytearray", func() { + f := fieldFromBytes([]byte("\x00\x00\x00\x00")) + So(f.size, ShouldEqual, 2) + So(len(f.cells), ShouldEqual, 4) + }) + + Convey("You can get an index from a point", func() { + So(f.i(Point{1, 1}), ShouldEqual, 3) + }) + + Convey("You can get a string representation of the field", func() { + So(f.String(), ShouldEqual, "\x00\x00\x00\x00") + }) + + Convey("You can manage the state of a cell in the field", func() { + So(f.state(p), ShouldBeFalse) + f.vivify(p) + So(f.state(p), ShouldBeTrue) + f.kill(p) + So(f.state(p), ShouldBeFalse) + }) + + Convey("You can manage the flags of a cell", func() { + f.setCorrect(p, true) + So(f.correct(p), ShouldBeTrue) + f.setComplete(p, true) + So(f.complete(p), ShouldBeTrue) + f.mark(p) + So(f.marked(p), ShouldBeTrue) + f.flag(p) + So(f.flagged(p), ShouldBeTrue) + f.clear(p) + So(f.correct(p), ShouldBeFalse) + So(f.complete(p), ShouldBeFalse) + So(f.marked(p), ShouldBeFalse) + So(f.flagged(p), ShouldBeFalse) + }) + }) +} diff --git a/state/state.go b/state/state.go new file mode 100644 index 0000000..78a3985 --- /dev/null +++ b/state/state.go @@ -0,0 +1,174 @@ +package state + +import ( + "bytes" + "math/rand" +) + +type Point struct { + X, Y int +} + +type State struct { + cursor *Point + + sectionSize, cellsPerSection int + cells, sections *field +} + +func New(sectionSize, cellsPerSection int) *State { + s := &State{ + sectionSize: sectionSize, + cellsPerSection: cellsPerSection, + cells: newField(sectionSize * cellsPerSection), + sections: newField(sectionSize), + cursor: &Point{0, 0}, + } + for x := 0; x < sectionSize; x++ { + for y := 0; y < sectionSize; y++ { + s.initSection(Point{x, y}) + } + } + return s +} + +// size is a utility method to keep cursor code clean. +func (s *State) size() int { + return s.sectionSize * s.cellsPerSection +} + +func (s *State) String() string { + var buf bytes.Buffer + for x := 0; x < s.sectionSize*s.cellsPerSection; x++ { + for y := 0; y < s.sectionSize*s.cellsPerSection; y++ { + p := Point{x, y} + if s.cells.correct(p) { + if s.cells.state(p) { + buf.WriteString("O") + } else { + buf.WriteString("X") + } + } else { + if s.cells.marked(p) { + buf.WriteString("o") + } else if s.cells.flagged(p) { + buf.WriteString("x") + } else { + buf.WriteString(" ") + } + } + } + buf.WriteString("\n") + } + return buf.String() +} + +func (s *State) Mark() []bool { + return s.mark(*s.cursor) +} + +func (s *State) Flag() []bool { + return s.flag(*s.cursor) +} + +func (s *State) Clear() { + s.clear(*s.cursor) +} + +func (s *State) view(cursor []int) { +} + +func (s *State) initSection(p Point) { + s.sections.clear(p) + startX := p.X * s.cellsPerSection + startY := p.Y * s.cellsPerSection + for x := 0; x < s.cellsPerSection; x++ { + for y := 0; y < s.cellsPerSection; y++ { + s.cells.clear(Point{x + startX, y + startY}) + if rand.Int()%2 == 0 { + s.cells.kill(Point{x + startX, y + startY}) + } else { + s.cells.vivify(Point{x + startX, y + startY}) + } + } + } +} + +func (s *State) mark(p Point) []bool { + s.cells.mark(p) + return s.update(p) +} + +func (s *State) flag(p Point) []bool { + s.cells.flag(p) + return s.update(p) +} + +func (s *State) clear(p Point) { + s.cells.clear(p) + s.update(p) +} + +func (s *State) sectionCorrect(p Point) bool { + sectionCorrect := true + for x := 0; x < s.cellsPerSection; x++ { + for y := 0; y < s.cellsPerSection; y++ { + sectionCorrect = sectionCorrect && s.cells.correct(Point{p.X + x, p.Y + y}) + } + } + return sectionCorrect +} + +func (s *State) updateCompletedSections() { + for x := 0; x < s.sectionSize; x++ { + for y := 0; y < s.sectionSize; y++ { + complete := true + for i := 0; i < s.sectionSize; i++ { + complete = complete && + s.sections.correct(Point{x, i}) && + s.sections.correct(Point{i, y}) + } + s.sections.setComplete(Point{x, y}, complete) + } + } +} + +func (s *State) clearValidCompletedSections() []bool { + cleared := make([]bool, s.sections.size*s.sections.size) + for x := 0; x < s.sectionSize; x++ { + for y := 0; y < s.sectionSize; y++ { + clearable := s.sections.complete(Point{x, y}) + s.sections.complete(Point{x + 1%s.sectionSize, y}) + s.sections.complete(Point{x, y + 1%s.sectionSize}) + s.sections.complete(Point{x + 1%s.sectionSize, y + 1%s.sectionSize}) + if clearable { + cleared[y*s.sectionSize+x] = true + cleared[y*s.sectionSize+(x+1%s.sectionSize)] = true + cleared[(y*s.sectionSize+s.sectionSize%len(cleared))+x] = true + cleared[(y*s.sectionSize+s.sectionSize%len(cleared))+(x+1%s.sectionSize)] = true + } + } + } + for i, v := range cleared { + if v { + s.initSection(Point{i % s.sectionSize, i / s.sectionSize}) + } + } + return cleared +} + +func (s *State) update(p Point) []bool { + sectionPoint := Point{p.X / s.cellsPerSection, p.Y / s.cellsPerSection} + + if s.cells.correct(p) && s.sectionCorrect(sectionPoint) { + s.sections.setCorrect(sectionPoint, true) + s.updateCompletedSections() + return s.clearValidCompletedSections() + } + s.sections.setCorrect(sectionPoint, false) + for i := 0; i < s.sectionSize; i++ { + s.sections.setComplete(Point{sectionPoint.X, i}, false) + s.sections.setComplete(Point{i, sectionPoint.Y}, false) + } + return make([]bool, s.sections.size*s.sections.size) +} diff --git a/state/state_test.go b/state/state_test.go new file mode 100644 index 0000000..44780a3 --- /dev/null +++ b/state/state_test.go @@ -0,0 +1,43 @@ +package state + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestState(t *testing.T) { + Convey("Given a game state", t, func() { + s := New(2, 2) + + Convey("You can get the size", func() { + So(s.size(), ShouldEqual, 4) + }) + + Convey("You can render a simple string of the board", func() { + s.cells.vivify(Point{0, 0}) + s.cells.kill(Point{0, 1}) + s.cells.vivify(Point{1, 0}) + s.cells.kill(Point{1, 1}) + + Convey("It shows correct guesses", func() { + s.Mark() + s.flag(Point{0, 1}) + So(s.String(), ShouldEqual, "OX \n \n \n \n") + }) + + Convey("It shows incorrect guesses", func() { + s.Flag() + s.mark(Point{0, 1}) + So(s.String(), ShouldEqual, "xo \n \n \n \n") + }) + + Convey("It shows cleared guesses as empty", func() { + s.Flag() + s.mark(Point{0, 1}) + s.Clear() + So(s.String(), ShouldEqual, " o \n \n \n \n") + }) + }) + }) +}