diff --git a/internal/game/game.go b/internal/game/game.go index 52a87ad..b20a3f3 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -112,7 +112,7 @@ func (g *Game) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch g.mode { case ModeSmart: if g.smartVal == -1 { - val := g.pattern.Tree.Get(msg.X, msg.Y, 0).Value() + val := g.pattern.Tree.Get(image.Pt(msg.X, msg.Y), 0).Value() switch val { case 0: g.smartVal = 1 @@ -120,11 +120,11 @@ func (g *Game) Update(msg tea.Msg) (tea.Model, tea.Cmd) { g.smartVal = 0 } } - g.pattern.Tree = g.pattern.Tree.Set(msg.X, msg.Y, g.smartVal) + g.pattern.Tree = g.pattern.Tree.Set(image.Pt(msg.X, msg.Y), g.smartVal) case ModePlace: - g.pattern.Tree = g.pattern.Tree.Set(msg.X, msg.Y, 1) + g.pattern.Tree = g.pattern.Tree.Set(image.Pt(msg.X, msg.Y), 1) case ModeErase: - g.pattern.Tree = g.pattern.Tree.Set(msg.X, msg.Y, 0) + g.pattern.Tree = g.pattern.Tree.Set(image.Pt(msg.X, msg.Y), 0) } } case tea.MouseButtonWheelUp: diff --git a/internal/pattern/plaintext.go b/internal/pattern/plaintext.go index 6a81743..dbc61de 100644 --- a/internal/pattern/plaintext.go +++ b/internal/pattern/plaintext.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "fmt" + "image" "io" "github.com/gabe565/cli-of-life/internal/quadtree" @@ -16,7 +17,7 @@ func UnmarshalPlaintext(r io.Reader) (Pattern, error) { Tree: quadtree.Empty(quadtree.DefaultTreeSize), } scanner := bufio.NewScanner(r) - var y int + var p image.Point for scanner.Scan() { line := scanner.Bytes() switch { @@ -32,21 +33,21 @@ func UnmarshalPlaintext(r io.Reader) (Pattern, error) { pattern.Comment += string(comment) } default: - var x int - pattern.Tree = pattern.Tree.GrowToFit(x, len(line)) + pattern.Tree = pattern.Tree.GrowToFit(p.Add(image.Pt(0, len(line)))) for _, b := range line { switch b { case '.': - pattern.Tree = pattern.Tree.Set(x, y, 0) - x++ + pattern.Tree = pattern.Tree.Set(p, 0) + p.X++ case 'O', '*': - pattern.Tree = pattern.Tree.Set(x, y, 1) - x++ + pattern.Tree = pattern.Tree.Set(p, 1) + p.X++ default: return pattern, fmt.Errorf("plaintext: %w: %q in line %q", ErrUnexpectedCharacter, string(b), line) } } - y++ + p.X = 0 + p.Y++ } } if scanner.Err() != nil { diff --git a/internal/pattern/rle.go b/internal/pattern/rle.go index 4baaced..f9bdaa4 100644 --- a/internal/pattern/rle.go +++ b/internal/pattern/rle.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "fmt" + "image" "io" "regexp" "strconv" @@ -21,7 +22,7 @@ func UnmarshalRLE(r io.Reader) (Pattern, error) { Tree: quadtree.Empty(quadtree.DefaultTreeSize), } scanner := bufio.NewScanner(r) - var x, y int + var p image.Point scan: for scanner.Scan() { line := scanner.Bytes() @@ -71,7 +72,7 @@ scan: } } - pattern.Tree = pattern.Tree.GrowToFit(w, h) + pattern.Tree = pattern.Tree.GrowToFit(image.Pt(w, h)) default: if len(line) == 0 { continue @@ -85,9 +86,9 @@ scan: runCount += int(b - '0') case b == '$': runCount = max(runCount, 1) - if x != 0 || y != 0 { - y += runCount - x = 0 + if p.X != 0 || p.Y != 0 { + p.Y += runCount + p.X = 0 } runCount = 0 case b == '!': @@ -97,14 +98,14 @@ scan: switch b { case 'b': for range runCount { - pattern.Tree = pattern.Tree.Set(x, y, 0) - x++ + pattern.Tree = pattern.Tree.Set(p, 0) + p.X++ } case ' ': default: for range runCount { - pattern.Tree = pattern.Tree.Set(x, y, 1) - x++ + pattern.Tree = pattern.Tree.Set(p, 1) + p.X++ } } runCount = 0 diff --git a/internal/quadtree/game_of_life.go b/internal/quadtree/game_of_life.go index 4db2309..54aa63c 100644 --- a/internal/quadtree/game_of_life.go +++ b/internal/quadtree/game_of_life.go @@ -1,6 +1,7 @@ package quadtree import ( + "image" "slices" "github.com/gabe565/cli-of-life/internal/rule" @@ -67,7 +68,7 @@ func (n *Node) slowSimulation(r *rule.Rule) *Node { var b uint16 for y := -2; y < 2; y++ { for x := -2; x < 2; x++ { - b = (b << 1) + uint16(n.Get(x, y, 0).value) + b = (b << 1) + uint16(n.Get(image.Pt(x, y), 0).value) } } return memoizedNew.Call(Children{NW: oneGen(b>>5, r), NE: oneGen(b>>4, r), SW: oneGen(b>>1, r), SE: oneGen(b, r)}) diff --git a/internal/quadtree/quadtree.go b/internal/quadtree/quadtree.go index 48d3eb8..89a2053 100644 --- a/internal/quadtree/quadtree.go +++ b/internal/quadtree/quadtree.go @@ -83,20 +83,20 @@ func (n *Node) grow() *Node { }) } -func (n *Node) GrowToFit(x, y int) *Node { +func (n *Node) GrowToFit(p image.Point) *Node { w := n.Width() / 2 - for x > w || y > w || x < -w || y < -w { + for p.X > w || p.Y > w || p.X < -w || p.Y < -w { n = n.grow() w = n.Width() / 2 } return n } -func (n *Node) Set(x, y int, value int) *Node { +func (n *Node) Set(p image.Point, value int) *Node { if n.level == 0 { switch { - case x < -1, x > 0, y < -1, y > 0: - panic(fmt.Sprintf("Reached leaf node with coordinates too big: (%d, %d)", x, y)) + case p.X < -1, p.X > 0, p.Y < -1, p.Y > 0: + panic(fmt.Sprintf("Reached leaf node with coordinates too big: (%d, %d)", p.X, p.Y)) case value == n.value: return n case value == 0: @@ -108,17 +108,17 @@ func (n *Node) Set(x, y int, value int) *Node { w := 1 << (n.level - 2) switch { - case x >= 0: + case p.X >= 0: switch { - case y >= 0: - return memoizedNew.Call(Children{NW: n.NW, NE: n.NE, SW: n.SW, SE: n.SE.Set(x-w, y-w, value)}) + case p.Y >= 0: + return memoizedNew.Call(Children{NW: n.NW, NE: n.NE, SW: n.SW, SE: n.SE.Set(p.Sub(image.Pt(w, w)), value)}) default: - return memoizedNew.Call(Children{NW: n.NW, NE: n.NE.Set(x-w, y+w, value), SW: n.SW, SE: n.SE}) + return memoizedNew.Call(Children{NW: n.NW, NE: n.NE.Set(p.Add(image.Pt(-w, w)), value), SW: n.SW, SE: n.SE}) } - case y >= 0: - return memoizedNew.Call(Children{NW: n.NW, NE: n.NE, SW: n.SW.Set(x+w, y-w, value), SE: n.SE}) + case p.Y >= 0: + return memoizedNew.Call(Children{NW: n.NW, NE: n.NE, SW: n.SW.Set(p.Add(image.Pt(w, -w)), value), SE: n.SE}) default: - return memoizedNew.Call(Children{NW: n.NW.Set(x+w, y+w, value), NE: n.NE, SW: n.SW, SE: n.SE}) + return memoizedNew.Call(Children{NW: n.NW.Set(p.Add(image.Pt(w, w)), value), NE: n.NE, SW: n.SW, SE: n.SE}) } } @@ -126,31 +126,31 @@ func (n *Node) children() []*Node { return []*Node{n.SE, n.SW, n.NW, n.NE} } -func (n *Node) Get(x, y int, level uint8) *Node { +func (n *Node) Get(p image.Point, level uint8) *Node { if n.level == level { allowed := 1 if level != 0 { allowed = 1 << (level - 1) } - if x < -allowed || x > allowed || y < -allowed || y > allowed { - panic(fmt.Sprintf("Reached leaf node with coordinates too big: (%d, %d)", x, y)) + if p.X < -allowed || p.X > allowed || p.Y < -allowed || p.Y > allowed { + panic(fmt.Sprintf("Reached leaf node with coordinates too big: (%d, %d)", p.X, p.Y)) } return n } w := 1 << (n.level - 2) switch { - case x >= 0: + case p.X >= 0: switch { - case y >= 0: - return n.SE.Get(x-w, y-w, level) + case p.Y >= 0: + return n.SE.Get(p.Sub(image.Pt(w, w)), level) default: - return n.NE.Get(x-w, y+w, level) + return n.NE.Get(p.Add(image.Pt(-w, w)), level) } - case y >= 0: - return n.SW.Get(x+w, y-w, level) + case p.Y >= 0: + return n.SW.Get(p.Add(image.Pt(w, -w)), level) default: - return n.NW.Get(x+w, y+w, level) + return n.NW.Get(p.Add(image.Pt(w, w)), level) } } @@ -214,7 +214,7 @@ func (n *Node) ToSlice() [][]int { for y := coords.Min.Y; y < coords.Max.Y; y++ { for x := coords.Min.X; x < coords.Max.X; x++ { - result[y-coords.Min.Y][x-coords.Min.X] = n.Get(x, y, 0).value + result[y-coords.Min.Y][x-coords.Min.X] = n.Get(image.Pt(x, y), 0).value } } return result diff --git a/internal/quadtree/quadtree_helpers_test.go b/internal/quadtree/quadtree_helpers_test.go index 8f7810c..6d0724d 100644 --- a/internal/quadtree/quadtree_helpers_test.go +++ b/internal/quadtree/quadtree_helpers_test.go @@ -1,6 +1,7 @@ package quadtree import ( + "image" "math/big" "math/rand" "testing" @@ -26,11 +27,9 @@ func treeWithRandomPattern(level uint) (*Node, *big.Int) { for x := range edgeLength { for y := range edgeLength { bitPosition := x*edgeLength + y - ux := x - edgeLength/2 - uy := y - edgeLength/2 - if randomNumber.Bit(bitPosition) != 0 { - node = node.Set(ux, uy, 1) + p := image.Pt(x-edgeLength/2, y-edgeLength/2) + node = node.Set(p, 1) } } } @@ -58,8 +57,8 @@ func treeCorrectness(t *testing.T, node *Node) { // 1 | 0 func slashLevelOne() *Node { return Empty(1). - Set(0, -1, 1). - Set(-1, 0, 1) + Set(image.Pt(0, -1), 1). + Set(image.Pt(-1, 0), 1) } // backslashLevelOne returns a level one tree with the following pattern @@ -67,6 +66,6 @@ func slashLevelOne() *Node { // 0 | 1 func backslashLevelOne() *Node { return Empty(1). - Set(0, 0, 1). - Set(-1, -1, 1) + Set(image.Pt(0, 0), 1). + Set(image.Pt(-1, -1), 1) } diff --git a/internal/quadtree/quadtree_test.go b/internal/quadtree/quadtree_test.go index 62209d7..59d7600 100644 --- a/internal/quadtree/quadtree_test.go +++ b/internal/quadtree/quadtree_test.go @@ -30,16 +30,16 @@ func TestEmpty(t *testing.T) { func TestNode_GrowToFit(t *testing.T) { node := Empty(1). - GrowToFit(63, 63) + GrowToFit(image.Pt(63, 63)) assert.EqualValues(t, 7, node.level) treeCorrectness(t, node) } func TestNode_Set(t *testing.T) { t.Run("panics", func(t *testing.T) { - node := Empty(1).GrowToFit(3, 3) + node := Empty(1).GrowToFit(image.Pt(3, 3)) assert.Panics(t, func() { - node = node.Set(8, 8, 1) + node = node.Set(image.Pt(8, 8), 1) }) }) @@ -47,31 +47,31 @@ func TestNode_Set(t *testing.T) { node := Empty(1) for i := range 10 { x, y := i-5*3, i-5*i - node = node.GrowToFit(x, y).Set(x, y, 1) - assert.EqualValues(t, 1, node.Get(x, y, 0).value) - node = node.Set(x, y, 0) - assert.EqualValues(t, 0, node.Get(x, y, 0).value) + node = node.GrowToFit(image.Pt(x, y)).Set(image.Pt(x, y), 1) + assert.EqualValues(t, 1, node.Get(image.Pt(x, y), 0).value) + node = node.Set(image.Pt(x, y), 0) + assert.EqualValues(t, 0, node.Get(image.Pt(x, y), 0).value) } // check that not all cells get set - node = node.Set(1, 1, 1) - assert.Equal(t, 0, node.Get(2, 2, 0).value) + node = node.Set(image.Pt(1, 1), 1) + assert.Equal(t, 0, node.Get(image.Pt(2, 2), 0).value) }) } func TestNode_Get(t *testing.T) { - node := Empty(1).GrowToFit(55, 233) - assert.Equal(t, 0, node.Get(55, 233, 0).value) - node = node.Set(55, 233, 1) - assert.Equal(t, 1, node.Get(55, 233, 0).value) + node := Empty(1).GrowToFit(image.Pt(55, 233)) + assert.Equal(t, 0, node.Get(image.Pt(55, 233), 0).value) + node = node.Set(image.Pt(55, 233), 1) + assert.Equal(t, 1, node.Get(image.Pt(55, 233), 0).value) treeCorrectness(t, node) } func TestNode_Visit(t *testing.T) { node := Empty(1). - GrowToFit(55, 233). - Set(55, 232, 1). - Set(55, 233, 1) + GrowToFit(image.Pt(55, 233)). + Set(image.Pt(55, 232), 1). + Set(image.Pt(55, 233), 1) var callCount int node.Visit(func(p image.Point, node *Node) { switch callCount { @@ -121,8 +121,8 @@ func Test_oneGen(t *testing.T) { func TestNode_centeredSubnode(t *testing.T) { node := Empty(3). - Set(1, 1, 1). - Set(-1, -1, 1) + Set(image.Pt(1, 1), 1). + Set(image.Pt(-1, -1), 1) center := node.centeredSubnode().grow() assert.Equal(t, node, center) } @@ -130,16 +130,16 @@ func TestNode_centeredSubnode(t *testing.T) { func TestNode_centeredNHorizontal(t *testing.T) { t.Run("backslash", func(t *testing.T) { node := Empty(3). - Set(-1, -3, 1). - Set(0, -2, 1). + Set(image.Pt(-1, -3), 1). + Set(image.Pt(0, -2), 1). centeredNHorizontal() assert.Equal(t, backslashLevelOne(), node) }) t.Run("slash", func(t *testing.T) { node := Empty(3). - Set(0, -3, 1). - Set(-1, -2, 1). + Set(image.Pt(0, -3), 1). + Set(image.Pt(-1, -2), 1). centeredNHorizontal() assert.Equal(t, slashLevelOne(), node) }) @@ -148,16 +148,16 @@ func TestNode_centeredNHorizontal(t *testing.T) { func TestNode_centeredSHorizontal(t *testing.T) { t.Run("backslash", func(t *testing.T) { node := Empty(3). - Set(-1, 1, 1). - Set(0, 2, 1). + Set(image.Pt(-1, 1), 1). + Set(image.Pt(0, 2), 1). centeredSHorizontal() assert.Equal(t, backslashLevelOne(), node) }) t.Run("slash", func(t *testing.T) { node := Empty(3). - Set(0, 1, 1). - Set(-1, 2, 1). + Set(image.Pt(0, 1), 1). + Set(image.Pt(-1, 2), 1). centeredSHorizontal() assert.Equal(t, slashLevelOne(), node) }) @@ -166,16 +166,16 @@ func TestNode_centeredSHorizontal(t *testing.T) { func TestNode_centeredWVertical(t *testing.T) { t.Run("backslash", func(t *testing.T) { node := Empty(3). - Set(-3, -1, 1). - Set(-2, 0, 1). + Set(image.Pt(-3, -1), 1). + Set(image.Pt(-2, 0), 1). centeredWVertical() assert.Equal(t, backslashLevelOne(), node) }) t.Run("slash", func(t *testing.T) { node := Empty(3). - Set(-2, -1, 1). - Set(-3, 0, 1). + Set(image.Pt(-2, -1), 1). + Set(image.Pt(-3, 0), 1). centeredWVertical() assert.Equal(t, slashLevelOne(), node) }) @@ -184,16 +184,16 @@ func TestNode_centeredWVertical(t *testing.T) { func TestNode_centeredEVertical(t *testing.T) { t.Run("backslash", func(t *testing.T) { node := Empty(3). - Set(1, -1, 1). - Set(2, 0, 1). + Set(image.Pt(1, -1), 1). + Set(image.Pt(2, 0), 1). centeredEVertical() assert.Equal(t, backslashLevelOne(), node) }) t.Run("slash", func(t *testing.T) { node := Empty(3). - Set(2, -1, 1). - Set(1, 0, 1). + Set(image.Pt(2, -1), 1). + Set(image.Pt(1, 0), 1). centeredEVertical() assert.Equal(t, slashLevelOne(), node) }) @@ -217,16 +217,16 @@ func TestNode_slowSimulation(t *testing.T) { // 0 | 1 t.Run("SW empty", func(t *testing.T) { node := Empty(2). - Set(-1, -1, 1). - Set(0, -1, 1). - Set(0, 0, 1). + Set(image.Pt(-1, -1), 1). + Set(image.Pt(0, -1), 1). + Set(image.Pt(0, 0), 1). slowSimulation(&r) expect := Empty(1). - Set(0, 0, 1). - Set(-1, 0, 1). - Set(-1, -1, 1). - Set(0, -1, 1) + Set(image.Pt(0, 0), 1). + Set(image.Pt(-1, 0), 1). + Set(image.Pt(-1, -1), 1). + Set(image.Pt(0, -1), 1) assert.Equal(t, expect, node) // next generation should be full @@ -242,7 +242,7 @@ func TestNode_slowSimulation(t *testing.T) { node := Empty(2) for x := -2; x < 2; x++ { for y := -2; y < 2; y++ { - node = node.Set(x, y, 1) + node = node.Set(image.Pt(x, y), 1) } } node = node.slowSimulation(&r) @@ -276,21 +276,21 @@ func TestNode_FilledCoords(t *testing.T) { want image.Rectangle }{ {"empty", Empty(1), image.Rectangle{}}, - {"1 cell", Empty(1).Set(0, 0, 1), image.Rect(0, 0, 1, 1)}, + {"1 cell", Empty(1).Set(image.Pt(0, 0), 1), image.Rect(0, 0, 1, 1)}, { "square", Empty(2). - Set(0, 0, 1). - Set(0, 1, 1). - Set(1, 0, 1). - Set(1, 1, 1), + Set(image.Pt(0, 0), 1). + Set(image.Pt(0, 1), 1). + Set(image.Pt(1, 0), 1). + Set(image.Pt(1, 1), 1), image.Rect(0, 0, 2, 2), }, { "negative", Empty(3). - Set(-2, -2, 1). - Set(2, 2, 1), + Set(image.Pt(-2, -2), 1). + Set(image.Pt(2, 2), 1), image.Rect(-2, -2, 3, 3), }, } @@ -310,31 +310,31 @@ func TestNode_ToSlice(t *testing.T) { { "positive glider", Empty(3). - Set(1, 0, 1). - Set(2, 1, 1). - Set(0, 2, 1). - Set(1, 2, 1). - Set(2, 2, 1), + Set(image.Pt(1, 0), 1). + Set(image.Pt(2, 1), 1). + Set(image.Pt(0, 2), 1). + Set(image.Pt(1, 2), 1). + Set(image.Pt(2, 2), 1), [][]int{{0, 1, 0}, {0, 0, 1}, {1, 1, 1}}, }, { "split positive/negative glider", Empty(3). - Set(0, -1, 1). - Set(1, 0, 1). - Set(-1, 1, 1). - Set(0, 1, 1). - Set(1, 1, 1), + Set(image.Pt(0, -1), 1). + Set(image.Pt(1, 0), 1). + Set(image.Pt(-1, 1), 1). + Set(image.Pt(0, 1), 1). + Set(image.Pt(1, 1), 1), [][]int{{0, 1, 0}, {0, 0, 1}, {1, 1, 1}}, }, { "negative glider", Empty(3). - Set(-2, -3, 1). - Set(-1, -2, 1). - Set(-3, -1, 1). - Set(-2, -1, 1). - Set(-1, -1, 1), + Set(image.Pt(-2, -3), 1). + Set(image.Pt(-1, -2), 1). + Set(image.Pt(-3, -1), 1). + Set(image.Pt(-2, -1), 1). + Set(image.Pt(-1, -1), 1), [][]int{{0, 1, 0}, {0, 0, 1}, {1, 1, 1}}, }, } diff --git a/internal/quadtree/render.go b/internal/quadtree/render.go index f237ad1..d5676ae 100644 --- a/internal/quadtree/render.go +++ b/internal/quadtree/render.go @@ -50,7 +50,7 @@ func (n *Node) Render(buf *bytes.Buffer, rect image.Rectangle, level uint8) { current := -1 for y := rect.Min.Y; y < rect.Max.Y; y += skip { for x := rect.Min.X; x < rect.Max.X; x += skip { - node := n.Get(x, y, level) + node := n.Get(image.Pt(x, y), level) if node.value == current { consecutive++ } else {