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) }