Files
gogogogogram/state/state.go
Madison Rye Progress 1bf7e0c828 start on scoring in state
2025-09-22 22:14:57 -07:00

280 lines
6.5 KiB
Go

package state
import (
"bytes"
"fmt"
"math/rand"
)
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("(%d,%d)", p.X, p.Y)
}
type score struct {
Clears, Score, Factor int
Blackout []bool
}
type header struct {
alive []int
complete bool
}
type State struct {
cursor *Point
score score
history string
historyIndex int
sectionSize, cellsPerSection int
cells, sections *field
rowHeaders, colHeaders []header
}
func New(sectionSize, cellsPerSection int) *State {
s := &State{
sectionSize: sectionSize,
cellsPerSection: cellsPerSection,
cells: newField(sectionSize * cellsPerSection),
sections: newField(sectionSize),
cursor: &Point{0, 0},
}
s.history = fmt.Sprintf("g(%d,%d)", sectionSize, cellsPerSection)
for x := 0; x < sectionSize; x++ {
for y := 0; y < sectionSize; y++ {
s.initSection(Point{x, y})
}
}
// TODO reveal 1 row per section, 1 col per sections/2
return s
}
// size is a utility method to keep cursor code clean.
func (s *State) size() int {
return s.sectionSize * s.cellsPerSection
}
// String is a utility method for providing a simpler string representation in tests.
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) && (s.cells.marked(p) || s.cells.flagged(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 {
if s.cells.state(p) {
buf.WriteString(".")
} else {
buf.WriteString(" ")
}
}
}
}
buf.WriteString("\n")
}
return buf.String()
}
func (s *State) Score() score {
return s.score
}
func (s *State) Mark() {
s.history += "m"
s.mark(*s.cursor)
}
func (s *State) Flag() {
s.history += "f"
s.flag(*s.cursor)
}
func (s *State) Clear() {
s.history += "c"
s.clear(*s.cursor, true)
}
func (s *State) view(cursor []int) {
}
func (s *State) initSection(p Point) {
s.history += fmt.Sprintf("i%s", p)
s.sections.clear(p, false)
startX := p.X * s.cellsPerSection
startY := p.Y * s.cellsPerSection
for x := 0; x < s.cellsPerSection; x++ {
for y := 0; y < s.cellsPerSection; y++ {
currPoint := Point{x + startX, y + startY}
s.cells.clear(currPoint, true)
if rand.Int()%2 == 0 {
s.cells.kill(currPoint)
s.history += "x"
} else {
s.cells.vivify(currPoint)
s.history += "o"
}
}
}
s.update(p)
}
func (s *State) mark(p Point) {
s.cells.mark(p)
s.update(p)
}
func (s *State) flag(p Point) {
s.cells.flag(p)
s.update(p)
}
func (s *State) clear(p Point, deadCorrect bool) {
s.cells.clear(p, deadCorrect)
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*s.cellsPerSection + x, p.Y*s.cellsPerSection + 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) scoreValidCompletedSections() {
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)+(x+1%s.sectionSize))%len(cleared)] = true
}
}
}
// Increase scores
blackout := true
for i, v := range cleared {
if v {
s.initSection(Point{i % s.sectionSize, i / s.sectionSize})
s.score.Clears++
s.score.Score += s.score.Factor + 1
s.score.Blackout[i] = true
}
blackout = blackout && s.score.Blackout[i]
}
// If all sections have been cleared at least once, increase the factor, clear the tracker, and start over with tracking.
if blackout {
s.score.Blackout = make([]bool, s.sections.size*s.sections.size)
s.score.Factor++
}
}
// updateAllHeaders runs updateHeaders for every row/column.
func (s *State) updateAllHeaders() {
for i := 0; i < s.size(); i++ {
s.updateHeaders(Point{i, i})
}
}
// updateHeaders makes sure that the counts of alive cells in rows/columns are accurate, and whether or not the row/column is complete.
func (s *State) updateHeaders(p Point) {
var rowHeader, colHeader header
var rowCounter, colCounter int
rowHeader.complete = true
colHeader.complete = true
// Loop through each row/column that contains this point
for i := 0; i < s.size(); i++ {
rowHeader.complete = rowHeader.complete && s.cells.complete(Point{i, p.Y})
if s.cells.state(Point{i, p.Y}) {
rowCounter++
} else {
if rowCounter > 0 {
rowHeader.alive = append(rowHeader.alive, rowCounter)
}
rowCounter = 0
}
colHeader.complete = colHeader.complete && s.cells.complete(Point{p.X, i})
if s.cells.state(Point{p.X, i}) {
colCounter++
} else {
if colCounter > 0 {
colHeader.alive = append(colHeader.alive, colCounter)
}
colCounter = 0
}
}
// In case there are any living cells at the end of the row/column
if rowCounter > 0 {
rowHeader.alive = append(rowHeader.alive, rowCounter)
}
if colCounter > 0 {
colHeader.alive = append(colHeader.alive, colCounter)
}
s.rowHeaders[p.Y] = rowHeader
s.colHeaders[p.X] = colHeader
}
func (s *State) update(p Point) {
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()
s.scoreValidCompletedSections()
return
}
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)
}
s.updateHeaders(p)
}