diff --git a/state/history.go b/state/history.go new file mode 100644 index 0000000..7c9ea7f --- /dev/null +++ b/state/history.go @@ -0,0 +1,136 @@ +package state + +import "strconv" + +// History returns the gameplay history for replays +func (s *State) History() string { + return s.history +} + +func UnmarshalAll(history string) *State { + s := Unmarshal(history) + for { + if !s.Step() { + break + } + } + return s +} + +func Unmarshal(history string) *State { + return &State{ + history: history, + historyIndex: 0, + cursor: &Point{0, 0}, + } +} + +func (s *State) Step() bool { + if s.historyIndex >= len(s.history) { + return false + } + + switch s.history[s.historyIndex] { + case 'g': + s.historyStart() + case 'i': + s.historyInitSection() + case 'm': + s.mark(*s.cursor) + case 'f': + s.flag(*s.cursor) + case 'c': + s.clear(*s.cursor, true) + case 'r': + s.cursorCellRight() + case 'l': + s.cursorCellLeft() + case 'u': + s.cursorCellUp() + case 'd': + s.cursorCellDown() + case 'R': + s.cursorSectionRight() + case 'L': + s.cursorSectionLeft() + case 'U': + s.cursorSectionUp() + case 'D': + s.cursorSectionDown() + } + s.historyIndex++ + + return true +} + +func (s *State) historyStart() { + peek := s.historyIndex + 1 + p, peek := s.historyPoint(peek) + s.sectionSize = p.X + s.cellsPerSection = p.Y + s.cells = newField(s.sectionSize * s.cellsPerSection) + s.sections = newField(s.sectionSize) + s.cursor = &Point{0, 0} + s.historyIndex = peek +} + +func (s *State) historyInitSection() { + peek := s.historyIndex + 1 + p, peek := s.historyPoint(peek) + segment := []byte(s.history[peek : peek+s.cellsPerSection*s.cellsPerSection]) + for i, c := range segment { + curr := Point{ + p.X*s.cellsPerSection + (i % s.cellsPerSection), + p.Y*s.cellsPerSection + (i / s.cellsPerSection), + } + s.cells.clear(curr, true) + switch c { + case 'x': + s.cells.vivify(curr) + case 'o': + s.cells.kill(curr) + default: + break + } + peek++ + } + s.historyIndex = peek +} + +func (s *State) historyPoint(index int) (Point, int) { + var x, y string + + // Advance past paren + index++ + for { + switch s.history[index] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + x += string(s.history[index]) + index++ + continue + } + break + } + + // Advance past comma + index++ + for { + switch s.history[index] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + y += string(s.history[index]) + index++ + continue + } + break + } + + pX, err := strconv.Atoi(x) + if err != nil { + panic(err) + } + pY, err := strconv.Atoi(y) + if err != nil { + panic(err) + } + return Point{pX, pY}, index +} diff --git a/state/history_test.go b/state/history_test.go new file mode 100644 index 0000000..5d5b2c3 --- /dev/null +++ b/state/history_test.go @@ -0,0 +1,57 @@ +package state + +import ( + "regexp" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestHistory(t *testing.T) { + Convey("Given a game's history", t, func() { + + Convey("You can maintain a history of all actions", func() { + s := New(2, 2) + s.Flag() + s.Mark() + s.Clear() + s.CursorSectionRight() + s.CursorSectionLeft() + s.CursorSectionDown() + s.CursorSectionUp() + s.CursorCellRight() + s.CursorCellLeft() + s.CursorCellDown() + s.CursorCellUp() + + matches, err := regexp.Match(`g\(2,2\)(i\(\d,\d\)[xo]{4}){4}fmcRLDUrldu`, []byte(s.History())) + + So(err, ShouldBeNil) + So(matches, ShouldBeTrue) + }) + + 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) + + Convey("It sets the history", func() { + So(s.History(), ShouldEqual, history) + }) + + Convey("It correctly sets the field sizes", func() { + So(s.sectionSize, ShouldEqual, 2) + So(s.cellsPerSection, ShouldEqual, 2) + So(len(s.cells.cells), ShouldEqual, 16) + So(len(s.sections.cells), ShouldEqual, 4) + }) + + Convey("The cursor should be set to the appropriate coordinates after all movement", func() { + So(s.cursor, ShouldResemble, &Point{0, 0}) + }) + + Convey("The board should be appropriately marked and filled", func() { + So(s.String(), ShouldEqual, "X. \n \no .\n....\n") + }) + }) + }) +}