Full Code of kelindar/tile for AI

master c3a5eaf85efd cached
17 files
88.2 KB
28.5k tokens
191 symbols
1 requests
Download .txt
Repository: kelindar/tile
Branch: master
Commit: c3a5eaf85efd
Files: 17
Total size: 88.2 KB

Directory structure:
gitextract_bvjbbodm/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── grid.go
├── grid_test.go
├── path.go
├── path_test.go
├── point.go
├── point_test.go
├── store.go
├── store_test.go
├── view.go
└── view_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: [kelindar]


================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on: [push, pull_request]
env:
  GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
  GO111MODULE: "on"
jobs:
  test:
    name: Test with Coverage
    runs-on: ubuntu-latest
    steps:
      - name: Set up Go
        uses: actions/setup-go@v1
        with:
          go-version: 1.23
      - name: Check out code
        uses: actions/checkout@v2
      - name: Install dependencies
        run: |
          go mod download
      - name: Run Unit Tests
        run: |
          go test -race -covermode atomic -coverprofile=profile.cov ./...
      - name: Upload Coverage
        uses: shogo82148/actions-goveralls@v1
        with:
          path-to-profile: profile.cov


================================================
FILE: .gitignore
================================================


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Roman Atachiants 

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Tile: Data-Oriented 2D Grid Engine

<p align="center">
    <img width="200" height="100" src="./.github/logo.png">
    <br>
    <img src="https://img.shields.io/github/go-mod/go-version/kelindar/tile" alt="Go Version">
    <a href="https://pkg.go.dev/github.com/kelindar/tile"><img src="https://pkg.go.dev/badge/github.com/kelindar/tile" alt="PkgGoDev"></a>
    <a href="https://goreportcard.com/report/github.com/kelindar/tile"><img src="https://goreportcard.com/badge/github.com/kelindar/tile" alt="Go Report Card"></a>
    <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
    <a href="https://coveralls.io/github/kelindar/tile"><img src="https://coveralls.io/repos/github/kelindar/tile/badge.svg" alt="Coverage"></a>
</p>

This repository contains a 2D tile map engine which is built with data and cache friendly ways. My main goal here is to provide a simple, high performance library to handle large scale tile maps in games.

- **Compact**. Each tile value is 4 bytes long and each grid page is 64-bytes long, which means a grid of 3000x3000 should take around 64MB of memory.
- **Thread-safe**. The grid is thread-safe and can be updated through provided update function. This allows multiple goroutines to read/write to the grid concurrently without any contentions. There is a spinlock per tile page protecting tile access.
- **Views & observers**. When a tile on the grid is updated, viewers of the tile will be notified of the update and can react to the changes. The idea is to allow you to build more complex, reactive systems on top of the grid.
- **Zero-allocation** (or close to it) traversal of the grid. The grid is pre-allocated entirely and this library provides a few ways of traversing it.
- **Path-finding**. The library provides a A\* pathfinding algorithm in order to compute a path between two points, as well as a BFS-based position scanning which searches the map around a point.

_Disclaimer_: the API or the library is not final and likely to change. Also, since this is just a side project of mine, don't expect this to be updated very often but please contribute!

# Grid & Tiles

The main entry in this library is `Grid` which represents, as the name implies a 2 dimentional grid which is the container of `Tile` structs. The `Tile` is essentially a cursor to a particular x,y coordinate and contains the following

- Value `uint32` of the tile, that can be used for calculating navigation or quickly retrieve sprite index.
- State set of `T comparable` that can be used to add additional information such as objects present on the tile. These things cannot be used for pathfinding, but can be used as an index.

Granted, uint32 value a bit small. The reason for this is the data layout, which is organised in thread-safe pages of 3x3 tiles, with the total size of 64 bytes which should neatly fit onto a cache line of a CPU.

In order to create a new `Grid[T]`, you first need to call `NewGridOf[T]()` method which pre-allocates the required space and initializes the tile grid itself. For example, you can create a 1000x1000 grid as shown below. The type argument `T` sets the type of the state objects. In the example below we want to create a new grid with a set of strings.

```go
grid := tile.NewGridOf[string](1000, 1000)
```

The `Each()` method of the grid allows you to iterate through all of the tiles in the grid. It takes an iterator function which is then invoked on every tile.

```go
grid.Each(func(p Point, t tile.Tile[string]) {
    // ...
})
```

The `Within()` method of the grid allows you to iterate through a set of tiles within a bounding box, specified by the top-left and bottom-right points. It also takes an iterator function which is then invoked on every tile matching the filter.

```go
grid.Within(At(1, 1), At(5, 5), func(p Point, t tile.Tile[string]) {
    // ...
})
```

The `At()` method of the grid allows you to retrieve a tile at a specific `x,y` coordinate. It simply returns the tile and whether it was found in the grid or not.

```go
if tile, ok := grid.At(50, 100); ok {
    // ...
}
```

The `WriteAt()` method of the grid allows you to update a tile at a specific `x,y` coordinate. Since the `Grid` itself is thread-safe, this is the way to (a) make sure the tile update/read is not racing and (b) notify observers of a tile update (more about this below).

```go
grid.WriteAt(50, 100, tile.Value(0xFF))
```

The `Neighbors()` method of the grid allows you to get the direct neighbors at a particular `x,y` coordinate and it takes an iterator funcion which is called for each neighbor. In this implementation, we are only taking direct neighbors (top, left, bottom, right). You rarely will need to use this method, unless you are rolling out your own pathfinding algorithm.

```go
grid.Neighbors(50, 100, func(point tile.Point, t tile.Tile[string]) {
    // ...
})
```

The `MergeAt()` method of the grid allows you to atomically update a value given a current value of the tile. For example, if we want to increment the value of a tile we can call this method with a function that increments the value. Under the hood, the increment will be done using an atomic compare-and-swap operation.

```go
grid.MergeAt(50, 100, func(v Value) Value {
    v += 1
    return v
})
```

The `MaskAt()` method of the grid allows you to atomically update only some of the bits at a particular `x,y` coordinate. This operation is as well thread-safe, and is actually useful when you might have multiple goroutines updating a set of tiles, but various goroutines are responsible for the various parts of the tile data. You might have a system that updates only a first couple of tile flags and another system updates some other bits. By using this method, two goroutines can update the different bits of the same tile concurrently, without erasing each other's results, which would happen if you just call `WriteAt()`.

```go
// assume byte[0] of the tile is 0b01010001
grid.MaskAt(50, 100,
    0b00101110, // Only last 2 bits matter
    0b00000011 // Mask specifies that we want to update last 2 bits
)

// If the original is currently: 0b01010001
// ...the result result will be: 0b01010010
```

# Pathfinding

As mentioned in the introduction, this library provides a few grid search / pathfinding functions as well. They are implemented as methods on the same `Grid` structure as the rest of the functionnality. The main difference is that they may require some allocations (I'll try to minimize it further in the future), and require a cost function `func(Tile) uint16` which returns a "cost" of traversing a specific tile. For example if the tile is a "swamp" in your game, it may cost higher than moving on a "plain" tile. If the cost function returns `0`, the tile is then considered to be an impassable obstacle, which is a good choice for walls and such.

The `Path()` method is used for finding a way between 2 points, you provide it the from/to point as well as costing function and it returns the path, calculated cost and whether a path was found or not. Note of caution however, avoid running it between 2 points if no path exists, since it might need to scan the entire map to figure that out with the current implementation.

```go
from := At(1, 1)
goal := At(7, 7)
path, distance, found := m.Path(from, goal, func(v tile.Value) uint16{
    if isImpassable(v) {
        return 0
    }
    return 1
})
```

The `Around()` method provides you with the ability to do a breadth-first search around a point, by providing a limit distance for the search as well as a cost function and an iterator. This is a handy way of finding things that are around the player in your game.

```go
point  := At(50, 50)
radius := 5
m.Around(point, radius, func(v tile.Value) uint16{
    if isImpassable(v) {
        return 0
    }
    return 1
}, func(p tile.Point, t tile.Tile[string]) {
    // ... tile found
})
```

# Observers

Given that the `Grid` is mutable and you can make changes to it from various goroutines, I have implemented a way to "observe" tile changes through a `NewView()` method which creates an `Observer` and can be used to observe changes within a bounding box. For example, you might want your player to have a view port and be notified if something changes on the map so you can do something about it.

In order to use these observers, you need to first call the `NewView()` function and start polling from the `Inbox` channel which will contain the tile update notifications as they happen. This channel has a small buffer, but if not read it will block the update, so make sure you always poll everything from it. Note that `NewView[S, T]` takes two type parameters, the first one is the type of the state object and the second one is the type of the tile value. The state object is used to store additional information about the view itself, such as the name of the view or a pointer to a socket that is used to send updates to the client.

In the example below we create a new 20x20 view on the grid and iterate through all of the tiles in the view.

```go
view := tile.NewView[string, string](grid, "My View #1")
view.Resize(tile.NewRect(0, 0, 20, 20), func(p tile.Point, t tile.Tile){
    // Optional, all of the tiles that are in the view now
})

// Poll the inbox (in reality this would need to be with a select, and a goroutine)
for {
    update := <-view.Inbox
    // Do something with update.Point, update.Tile
}
```

The `MoveBy()` method allows you to move the view in a specific direction. It takes in a `x,y` vector but it can contain negative values. In the example below, we move the view upwards by 5 tiles. In addition, we can also provide an iterator and do something with all of the tiles that have entered the view (e.g. show them to the player).

```go
view.MoveBy(0, 5, func(p tile.Point, tile tile.Tile){
    // Every tile which entered our view
})
```

Similarly, `MoveAt()` method allows you to move the view at a specific location provided by the coordinates. The size of the view stays the same and the iterator will be called for all of the new tiles that have entered the view port.

```go
view.MoveAt(At(10, 10), func(p tile.Point, t tile.Tile){
    // Every tile which entered our view
})
```

The `Resize()` method allows you to resize and update the view port. As usual, the iterator will be called for all of the new tiles that have entered the view port.

```go
viewRect := tile.NewRect(10, 10, 30, 30)
view.Resize(viewRect, func(p tile.Point, t tile.Tile){
    // Every tile which entered our view
})
```

The `Close()` method should be called when you are done with the view, since it unsubscribes all of the notifications. Be careful, if you do not close the view when you are done with it, it will lead to memory leaks since it will continue to observe the grid and receive notifications.

```go
// Unsubscribe from notifications and close the view
view.Close()
```

# Save & Load

The library also provides a way to save the `Grid` to an `io.Writer` and load it from an `io.Reader` by using `WriteTo()` method and `ReadFrom()` function. Keep in mind that the save/load mechanism does not do any compression, but in practice you should [use to a compressor](https://github.com/klauspost/compress) if you want your maps to not take too much of the disk space - snappy is a good option for this since it's fast and compresses relatively well.

The `WriteTo()` method of the grid only requires a specific `io.Writer` to be passed and returns a number of bytes that have been written down to it as well if any specific error has occured. Below is an example of how to save the grid into a compressed buffer.

```go
// Prepare the output buffer and compressor
output := new(bytes.Buffer)
writer, err := flate.NewWriter(output, flate.BestSpeed)
if err != nil {
    // ...
}

defer writer.Close()            // Make sure we flush the compressor
_, err := grid.WriteTo(writer)  // Write the grid
if err != nil {
    // ...
}
```

The `ReadFrom()` function allows you to read the `Grid` from a particular reader. To complement the example above, the one below shows how to read a compressed grid using this function.

```go
// Prepare a compressed reader over the buffer
reader := flate.NewReader(output)

// Read the Grid
grid, err := ReadFrom(reader)
if err != nil{
    // ...
}
```

# Benchmarks

This library contains quite a bit of various micro-benchmarks to make sure that everything stays pretty fast. Feel free to clone and play around with them yourself. Below are the benchmarks which we have, most of them are running on relatively large grids.

```
cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkGrid/each-8                 868    1358434 ns/op           0 B/op   0 allocs/op
BenchmarkGrid/neighbors-8       66551679      17.87 ns/op           0 B/op   0 allocs/op
BenchmarkGrid/within-8             27207      44753 ns/op           0 B/op   0 allocs/op
BenchmarkGrid/at-8             399067512      2.994 ns/op           0 B/op   0 allocs/op
BenchmarkGrid/write-8          130207965      9.294 ns/op           0 B/op   0 allocs/op
BenchmarkGrid/merge-8          124156794      9.663 ns/op           0 B/op   0 allocs/op
BenchmarkGrid/mask-8           100000000      10.67 ns/op           0 B/op   0 allocs/op
BenchmarkState/range-8          12106854      98.91 ns/op           0 B/op   0 allocs/op
BenchmarkState/add-8            48827727      25.43 ns/op           0 B/op   0 allocs/op
BenchmarkState/del-8            52110474      21.59 ns/op           0 B/op   0 allocs/op
BenchmarkPath/9x9-8               264586        4656 ns/op      16460 B/op   3 allocs/op
BenchmarkPath/300x300-8              601     1937662 ns/op    7801502 B/op   4 allocs/op
BenchmarkPath/381x381-8              363     3304134 ns/op   62394356 B/op   5 allocs/op
BenchmarkPath/384x384-8              171     7165777 ns/op   62394400 B/op   5 allocs/op
BenchmarkPath/3069x3069-8             31    36479106 ns/op  124836075 B/op   4 allocs/op
BenchmarkPath/3072x3072-8             30    34889740 ns/op  124837686 B/op   4 allocs/op
BenchmarkPath/6144x6144-8            142     7594013 ns/op   62395376 B/op   3 allocs/op
BenchmarkAround/3r-8              506857        2384 ns/op        385 B/op   1 allocs/op
BenchmarkAround/5r-8              214280        5539 ns/op        922 B/op   2 allocs/op
BenchmarkAround/10r-8              85723       14017 ns/op       3481 B/op   2 allocs/op
BenchmarkPoint/within-8       1000000000      0.2190 ns/op          0 B/op   0 allocs/op
BenchmarkPoint/within-rect-8  1000000000      0.2195 ns/op          0 B/op   0 allocs/op
BenchmarkStore/save-8              14577       82510 ns/op          8 B/op   1 allocs/op
BenchmarkStore/read-8               3199      364771 ns/op     647419 B/op   7 allocs/op
BenchmarkView/write-8            6285351       188.2 ns/op         48 B/op   1 allocs/op
BenchmarkView/move-8               10000      116953 ns/op          0 B/op   0 allocs/op
```

# Contributing

We are open to contributions, feel free to submit a pull request and we'll review it as quickly as we can. This library is maintained by [Roman Atachiants](https://www.linkedin.com/in/atachiants/)

## License

Tile is licensed under the [MIT License](LICENSE.md).


================================================
FILE: go.mod
================================================
module github.com/kelindar/tile

go 1.25

require (
	github.com/kelindar/intmap v1.5.0
	github.com/kelindar/iostream v1.4.0
	github.com/stretchr/testify v1.10.0
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kelindar/intmap v1.5.0 h1:VY+AdO4Wx1sF1vGiTkS8n2lxhmFgOQwCIFuePQP4Iqw=
github.com/kelindar/intmap v1.5.0/go.mod h1:NkypxhfaklmDTJqwano3Q1BWk6je77qgQwszDwu8Kc8=
github.com/kelindar/iostream v1.4.0 h1:ELKlinnM/K3GbRp9pYhWuZOyBxMMlYAfsOP+gauvZaY=
github.com/kelindar/iostream v1.4.0/go.mod h1:MkjMuVb6zGdPQVdwLnFRO0xOTOdDvBWTztFmjRDQkXk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: grid.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"sync"
	"sync/atomic"
)

// Grid represents a 2D tile map. Internally, a map is composed of 3x3 pages.
type Grid[T comparable] struct {
	pages      []page[T] // The pages of the map
	pageWidth  int16     // The max page width
	pageHeight int16     // The max page height
	observers  pubsub[T] // The map of observers
	Size       Point     // The map size
}

// NewGrid returns a new map of the specified size. The width and height must be both
// multiples of 3.
func NewGrid(width, height int16) *Grid[string] {
	return NewGridOf[string](width, height)
}

// NewGridOf returns a new map of the specified size. The width and height must be both
// multiples of 3.
func NewGridOf[T comparable](width, height int16) *Grid[T] {
	width, height = width/3, height/3

	max := int32(width) * int32(height)
	pages := make([]page[T], max)
	m := &Grid[T]{
		pages:      pages,
		pageWidth:  width,
		pageHeight: height,
		Size:       At(width*3, height*3),
		observers: pubsub[T]{
			tmp: sync.Pool{
				New: func() any { return make(map[Observer[T]]struct{}, 4) },
			},
		},
	}

	// Function to calculate a point based on the index
	var pointAt func(i int) Point = func(i int) Point {
		return At(int16(i%int(width)), int16(i/int(width)))
	}

	for i := 0; i < int(max); i++ {
		pages[i].point = pointAt(i).MultiplyScalar(3)
	}
	return m
}

// Each iterates over all of the tiles in the map.
func (m *Grid[T]) Each(fn func(Point, Tile[T])) {
	until := int(m.pageHeight) * int(m.pageWidth)
	for i := 0; i < until; i++ {
		m.pages[i].Each(m, fn)
	}
}

// Within selects the tiles within a specifid bounding box which is specified by
// north-west and south-east coordinates.
func (m *Grid[T]) Within(nw, se Point, fn func(Point, Tile[T])) {
	m.pagesWithin(nw, se, func(page *page[T]) {
		page.Each(m, func(p Point, v Tile[T]) {
			if p.Within(nw, se) {
				fn(p, v)
			}
		})
	})
}

// pagesWithin selects the pages within a specifid bounding box which is specified
// by north-west and south-east coordinates.
func (m *Grid[T]) pagesWithin(nw, se Point, fn func(*page[T])) {
	if !se.WithinSize(m.Size) {
		se = At(m.Size.X-1, m.Size.Y-1)
	}

	for x := nw.X / 3; x <= se.X/3; x++ {
		for y := nw.Y / 3; y <= se.Y/3; y++ {
			fn(m.pageAt(x, y))
		}
	}
}

// At returns the tile at a specified position
func (m *Grid[T]) At(x, y int16) (Tile[T], bool) {
	if x >= 0 && y >= 0 && x < m.Size.X && y < m.Size.Y {
		return m.pageAt(x/3, y/3).At(m, x, y), true
	}

	return Tile[T]{}, false
}

// WriteAt updates the entire tile value at a specific coordinate
func (m *Grid[T]) WriteAt(x, y int16, tile Value) {
	if x >= 0 && y >= 0 && x < m.Size.X && y < m.Size.Y {
		m.pageAt(x/3, y/3).writeTile(m, uint8((y%3)*3+(x%3)), tile)
	}
}

// MaskAt atomically updates the bits of tile at a specific coordinate. The bits are
// specified by the mask. The bits that need to be updated should be flipped on in the mask.
func (m *Grid[T]) MaskAt(x, y int16, tile, mask Value) {
	m.MergeAt(x, y, func(value Value) Value {
		return (value &^ mask) | (tile & mask)
	})
}

// Merge atomically merges the tile by applying a merging function at a specific coordinate.
func (m *Grid[T]) MergeAt(x, y int16, merge func(Value) Value) {
	if x >= 0 && y >= 0 && x < m.Size.X && y < m.Size.Y {
		m.pageAt(x/3, y/3).mergeTile(m, uint8((y%3)*3+(x%3)), merge)
	}
}

// Neighbors iterates over the direct neighbouring tiles
func (m *Grid[T]) Neighbors(x, y int16, fn func(Point, Tile[T])) {

	// First we need to figure out which pages contain the neighboring tiles and
	// then load them. In the best-case we need to load only a single page. In
	// the worst-case: we need to load 3 pages.
	nX, nY := x/3, (y-1)/3 // North
	eX, eY := (x+1)/3, y/3 // East
	sX, sY := x/3, (y+1)/3 // South
	wX, wY := (x-1)/3, y/3 // West

	// Get the North
	if y > 0 {
		fn(At(x, y-1), m.pageAt(nX, nY).At(m, x, y-1))
	}

	// Get the East
	if eX < m.pageWidth {
		fn(At(x+1, y), m.pageAt(eX, eY).At(m, x+1, y))
	}

	// Get the South
	if sY < m.pageHeight {
		fn(At(x, y+1), m.pageAt(sX, sY).At(m, x, y+1))
	}

	// Get the West
	if x > 0 {
		fn(At(x-1, y), m.pageAt(wX, wY).At(m, x-1, y))
	}
}

// pageAt loads a page at a given page location
func (m *Grid[T]) pageAt(x, y int16) *page[T] {
	index := int(x) + int(m.pageWidth)*int(y)

	// Eliminate bounds checks
	if index >= 0 && index < len(m.pages) {
		return &m.pages[index]
	}

	return nil
}

// ---------------------------------- Tile ----------------------------------

// Value represents a packed tile information, it must fit on 4 bytes.
type Value = uint32

// ---------------------------------- Page ----------------------------------

// page represents a 3x3 tile page each page should neatly fit on a cache
// line and speed things up.
type page[T comparable] struct {
	mu    sync.Mutex  // State lock, 8 bytes
	state map[T]uint8 // State data, 8 bytes
	flags uint32      // Page flags, 4 bytes
	point Point       // Page X, Y coordinate, 4 bytes
	tiles [9]Value    // Page tiles, 36 bytes
}

// tileAt reads a tile at a page index
func (p *page[T]) tileAt(idx uint8) Value {
	return Value(atomic.LoadUint32((*uint32)(&p.tiles[idx])))
}

// IsObserved returns whether the tile is observed or not
func (p *page[T]) IsObserved() bool {
	return (atomic.LoadUint32(&p.flags))&1 != 0
}

// Bounds returns the bounding box for the tile page.
func (p *page[T]) Bounds() Rect {
	return Rect{p.point, At(p.point.X+3, p.point.Y+3)}
}

// At returns a cursor at a specific coordinate
func (p *page[T]) At(grid *Grid[T], x, y int16) Tile[T] {
	return Tile[T]{grid: grid, data: p, idx: uint8((y%3)*3 + (x % 3))}
}

// Each iterates over all of the tiles in the page.
func (p *page[T]) Each(grid *Grid[T], fn func(Point, Tile[T])) {
	x, y := p.point.X, p.point.Y
	fn(Point{x, y}, Tile[T]{grid: grid, data: p, idx: 0})         // NW
	fn(Point{x + 1, y}, Tile[T]{grid: grid, data: p, idx: 1})     // N
	fn(Point{x + 2, y}, Tile[T]{grid: grid, data: p, idx: 2})     // NE
	fn(Point{x, y + 1}, Tile[T]{grid: grid, data: p, idx: 3})     // W
	fn(Point{x + 1, y + 1}, Tile[T]{grid: grid, data: p, idx: 4}) // C
	fn(Point{x + 2, y + 1}, Tile[T]{grid: grid, data: p, idx: 5}) // E
	fn(Point{x, y + 2}, Tile[T]{grid: grid, data: p, idx: 6})     // SW
	fn(Point{x + 1, y + 2}, Tile[T]{grid: grid, data: p, idx: 7}) // S
	fn(Point{x + 2, y + 2}, Tile[T]{grid: grid, data: p, idx: 8}) // SE
}

// SetObserved sets the observed flag on the page
func (p *page[T]) SetObserved(observed bool) {
	const flagObserved = 0x1
	for {
		value := atomic.LoadUint32(&p.flags)
		merge := value
		if observed {
			merge = value | flagObserved
		} else {
			merge = value &^ flagObserved
		}

		if atomic.CompareAndSwapUint32(&p.flags, value, merge) {
			break
		}
	}
}

// Lock locks the state. Note: this needs to be named Lock() so go vet will
// complain if the page is copied around.
func (p *page[T]) Lock() {
	p.mu.Lock()
}

// Unlock unlocks the state. Note: this needs to be named Unlock() so go vet will
// complain if the page is copied around.
func (p *page[T]) Unlock() {
	p.mu.Unlock()
}

// ---------------------------------- Mutations ----------------------------------

// writeTile stores the tile and return  whether tile is observed or not
func (p *page[T]) writeTile(grid *Grid[T], idx uint8, after Value) {
	before := p.tileAt(idx)
	for !atomic.CompareAndSwapUint32(&p.tiles[idx], uint32(before), uint32(after)) {
		before = p.tileAt(idx)
	}

	// If observed, notify the observers of the tile
	if p.IsObserved() {
		at := pointOf(p.point, idx)
		grid.observers.Notify1(&Update[T]{
			Old: ValueAt{
				Point: at,
				Value: before,
			},
			New: ValueAt{
				Point: at,
				Value: after,
			},
		}, p.point)
	}
}

// mergeTile atomically merges the tile bits given a function
func (p *page[T]) mergeTile(grid *Grid[T], idx uint8, fn func(Value) Value) Value {
	before := p.tileAt(idx)
	after := fn(before)

	// Swap, if we're not able to re-merge again
	for !atomic.CompareAndSwapUint32(&p.tiles[idx], uint32(before), uint32(after)) {
		before = p.tileAt(idx)
		after = fn(before)
	}

	// If observed, notify the observers of the tile
	if p.IsObserved() {
		at := pointOf(p.point, idx)
		grid.observers.Notify1(&Update[T]{
			Old: ValueAt{
				Point: at,
				Value: before,
			},
			New: ValueAt{
				Point: at,
				Value: after,
			},
		}, p.point)
	}

	// Return the merged tile data
	return after
}

// addObject adds object to the set
func (p *page[T]) addObject(idx uint8, object T) (value uint32) {
	p.Lock()

	// Lazily initialize the map, as most pages might not have anything stored
	// in them (e.g. water or empty tile)
	if p.state == nil {
		p.state = make(map[T]uint8)
	}

	p.state[object] = uint8(idx)
	value = p.tileAt(idx)
	p.Unlock()
	return
}

// delObject removes the object from the set
func (p *page[T]) delObject(idx uint8, object T) (value uint32) {
	p.Lock()
	if p.state != nil {
		delete(p.state, object)
	}
	value = p.tileAt(idx)
	p.Unlock()
	return
}

// ---------------------------------- Tile Cursor ----------------------------------

// Tile represents an iterator over all state objects at a particular location.
type Tile[T comparable] struct {
	grid *Grid[T] // grid pointer
	data *page[T] // page pointer
	idx  uint8    // tile index
}

// Count returns number of objects at the current tile.
func (t Tile[T]) Count() (count int) {
	t.data.Lock()
	defer t.data.Unlock()
	for _, idx := range t.data.state {
		if idx == uint8(t.idx) {
			count++
		}
	}
	return
}

// Point returns the point of the tile
func (t Tile[T]) Point() Point {
	return pointOf(t.data.point, t.idx)
}

// Value reads the tile information
func (t Tile[T]) Value() Value {
	return t.data.tileAt(t.idx)
}

// Range iterates over all of the objects in the set
func (t Tile[T]) Range(fn func(T) error) error {
	t.data.Lock()
	defer t.data.Unlock()
	for v, idx := range t.data.state {
		if idx == uint8(t.idx) {
			if err := fn(v); err != nil {
				return err
			}
		}
	}
	return nil
}

// IsObserved returns whether the tile is observed or not
func (t Tile[T]) IsObserved() bool {
	return t.data.IsObserved()
}

// Observers iterates over all views observing this tile
func (t Tile[T]) Observers(fn func(view Observer[T])) {
	if !t.data.IsObserved() {
		return
	}

	t.grid.observers.Each1(func(sub Observer[T]) {
		if sub.Viewport().Contains(t.Point()) {
			fn(sub)
		}
	}, t.data.point)
}

// Add adds object to the set
func (t Tile[T]) Add(v T) {
	value := t.data.addObject(t.idx, v)

	// If observed, notify the observers of the tile
	if t.data.IsObserved() {
		at := t.Point()
		t.grid.observers.Notify1(&Update[T]{
			Old: ValueAt{
				Point: at,
				Value: value,
			},
			New: ValueAt{
				Point: at,
				Value: value,
			},
			Add: v,
		}, t.data.point)
	}
}

// Del removes the object from the set
func (t Tile[T]) Del(v T) {
	value := t.data.delObject(t.idx, v)

	// If observed, notify the observers of the tile
	if t.data.IsObserved() {
		at := t.Point()
		t.grid.observers.Notify1(&Update[T]{
			Old: ValueAt{
				Point: at,
				Value: value,
			},
			New: ValueAt{
				Point: at,
				Value: value,
			},
			Del: v,
		}, t.data.point)
	}
}

// Move moves an object from the current tile to the destination tile.
func (t Tile[T]) Move(v T, dst Point) bool {
	d, ok := t.grid.At(dst.X, dst.Y)
	if !ok {
		return false
	}

	// Move the object from the source to the destination
	tv := t.data.delObject(d.idx, v)
	dv := d.data.addObject(d.idx, v)
	if !t.data.IsObserved() && !d.data.IsObserved() {
		return true
	}

	// Prepare the update notification
	update := &Update[T]{
		Old: ValueAt{
			Point: t.Point(),
			Value: tv,
		},
		New: ValueAt{
			Point: d.Point(),
			Value: dv,
		},
		Del: v,
		Add: v,
	}

	switch {
	case t.data == d.data || !d.data.IsObserved():
		t.grid.observers.Notify1(update, t.data.point)
	case !t.data.IsObserved():
		t.grid.observers.Notify1(update, d.data.point)
	default:
		t.grid.observers.Notify2(update, [2]Point{
			t.data.point,
			d.data.point,
		})
	}
	return true
}

// Write updates the entire tile value.
func (t Tile[T]) Write(tile Value) {
	t.data.writeTile(t.grid, t.idx, tile)
}

// Merge atomically merges the tile by applying a merging function.
func (t Tile[T]) Merge(merge func(Value) Value) Value {
	return t.data.mergeTile(t.grid, t.idx, merge)
}

// Mask updates the bits of tile. The bits are specified by the mask. The bits
// that need to be updated should be flipped on in the mask.
func (t Tile[T]) Mask(tile, mask Value) Value {
	return t.data.mergeTile(t.grid, t.idx, func(value Value) Value {
		return (value &^ mask) | (tile & mask)
	})
}

// pointOf returns the point given an index
func pointOf(page Point, idx uint8) Point {
	return Point{
		X: page.X + int16(idx)%3,
		Y: page.Y + int16(idx)/3,
	}
}


================================================
FILE: grid_test.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"fmt"
	"io"
	"sync"
	"testing"
	"unsafe"

	"github.com/stretchr/testify/assert"
)

/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkGrid/each-24         	    1452	    	830268 ns/op	       0 B/op	       0 allocs/op
BenchmarkGrid/neighbors-24    	121583491	         9.861 ns/op	       0 B/op	       0 allocs/op
BenchmarkGrid/within-24       	   49360	     	 24477 ns/op	       0 B/op	       0 allocs/op
BenchmarkGrid/at-24           	687659378	         1.741 ns/op	       0 B/op	       0 allocs/op
BenchmarkGrid/write-24        	191272338	         6.307 ns/op	       0 B/op	       0 allocs/op
BenchmarkGrid/merge-24        	162536985	         7.332 ns/op	       0 B/op	       0 allocs/op
BenchmarkGrid/mask-24         	158258084	         7.601 ns/op	       0 B/op	       0 allocs/op
*/
func BenchmarkGrid(b *testing.B) {
	var d Tile[uint32]
	var p Point
	defer assert.NotNil(b, d)
	m := NewGridOf[uint32](768, 768)

	b.Run("each", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Each(func(point Point, tile Tile[uint32]) {
				p = point
				d = tile
			})
		}
	})

	b.Run("neighbors", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Neighbors(300, 300, func(point Point, tile Tile[uint32]) {
				p = point
				d = tile
			})
		}
	})

	b.Run("within", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Within(At(100, 100), At(200, 200), func(point Point, tile Tile[uint32]) {
				p = point
				d = tile
			})
		}
	})

	assert.NotZero(b, p.X)
	b.Run("at", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			d, _ = m.At(100, 100)
		}
	})

	b.Run("write", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.WriteAt(100, 100, Value(0))
		}
	})

	b.Run("merge", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.MergeAt(100, 100, func(v Value) Value {
				v += 1
				return v
			})
		}
	})

	b.Run("mask", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.MaskAt(100, 100, Value(0), Value(1))
		}
	})
}

/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkState/range-24         	17017800	        71.14 ns/op	       0 B/op	       0 allocs/op
BenchmarkState/add-24           	72639224	        16.32 ns/op	       0 B/op	       0 allocs/op
BenchmarkState/del-24           	82469125	        13.65 ns/op	       0 B/op	       0 allocs/op
*/
func BenchmarkState(b *testing.B) {
	m := NewGridOf[int](768, 768)
	m.Each(func(p Point, c Tile[int]) {
		for i := 0; i < 10; i++ {
			c.Add(i)
		}
	})

	b.Run("range", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			cursor, _ := m.At(100, 100)
			cursor.Range(func(v int) error {
				return nil
			})
		}
	})

	b.Run("add", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			cursor, _ := m.At(100, 100)
			cursor.Add(100)
		}
	})

	b.Run("del", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			cursor, _ := m.At(100, 100)
			cursor.Del(100)
		}
	})
}

func TestPageSize(t *testing.T) {
	assert.Equal(t, 8, int(unsafe.Sizeof(map[uintptr]Point{})))
	assert.Equal(t, 64, int(unsafe.Sizeof(page[string]{})))
	assert.Equal(t, 36, int(unsafe.Sizeof([9]Value{})))
}

func TestWithin(t *testing.T) {
	m := NewGrid(9, 9)

	var path []string
	m.Within(At(1, 1), At(5, 5), func(p Point, tile Tile[string]) {
		path = append(path, p.String())
	})
	assert.Equal(t, 16, len(path))
	assert.ElementsMatch(t, []string{
		"1,1", "2,1", "1,2", "2,2",
		"3,1", "4,1", "3,2", "4,2",
		"1,3", "2,3", "1,4", "2,4",
		"3,3", "4,3", "3,4", "4,4",
	}, path)
}

func TestWithinCorner(t *testing.T) {
	m := NewGrid(9, 9)

	var path []string
	m.Within(At(7, 6), At(10, 10), func(p Point, tile Tile[string]) {
		path = append(path, p.String())
	})
	assert.Equal(t, 6, len(path))
	assert.ElementsMatch(t, []string{
		"7,6", "8,6", "7,7",
		"8,7", "7,8", "8,8",
	}, path)
}

func TestWithinXY(t *testing.T) {
	assert.False(t, At(4, 8).WithinRect(NewRect(1, 6, 4, 10)))
}

func TestWithinOneSide(t *testing.T) {
	m := NewGrid(9, 9)

	var path []string
	m.Within(At(1, 6), At(4, 10), func(p Point, tile Tile[string]) {
		path = append(path, p.String())
	})
	assert.Equal(t, 9, len(path))
	assert.ElementsMatch(t, []string{
		"1,6", "2,6", "3,6",
		"1,7", "2,7", "3,7",
		"1,8", "2,8", "3,8",
	}, path)
}

func TestWithinInvalid(t *testing.T) {
	m := NewGrid(9, 9)
	count := 0
	m.Within(At(10, 10), At(20, 20), func(p Point, tile Tile[string]) {
		count++
	})
	assert.Equal(t, 0, count)
}

func TestEach(t *testing.T) {
	m := NewGrid(9, 9)

	var path []string
	m.Each(func(p Point, tile Tile[string]) {
		path = append(path, p.String())
	})
	assert.Equal(t, 81, len(path))
	assert.ElementsMatch(t, []string{
		"0,0", "1,0", "2,0", "0,1", "1,1", "2,1", "0,2", "1,2", "2,2",
		"0,3", "1,3", "2,3", "0,4", "1,4", "2,4", "0,5", "1,5", "2,5",
		"0,6", "1,6", "2,6", "0,7", "1,7", "2,7", "0,8", "1,8", "2,8",
		"3,0", "4,0", "5,0", "3,1", "4,1", "5,1", "3,2", "4,2", "5,2",
		"3,3", "4,3", "5,3", "3,4", "4,4", "5,4", "3,5", "4,5", "5,5",
		"3,6", "4,6", "5,6", "3,7", "4,7", "5,7", "3,8", "4,8", "5,8",
		"6,0", "7,0", "8,0", "6,1", "7,1", "8,1", "6,2", "7,2", "8,2",
		"6,3", "7,3", "8,3", "6,4", "7,4", "8,4", "6,5", "7,5", "8,5",
		"6,6", "7,6", "8,6", "6,7", "7,7", "8,7", "6,8", "7,8", "8,8",
	}, path)
}

func TestNeighbors(t *testing.T) {
	tests := []struct {
		x, y   int16
		expect []string
	}{
		{x: 0, y: 0, expect: []string{"1,0", "0,1"}},
		{x: 1, y: 0, expect: []string{"2,0", "1,1", "0,0"}},
		{x: 1, y: 1, expect: []string{"1,0", "2,1", "1,2", "0,1"}},
		{x: 2, y: 2, expect: []string{"2,1", "3,2", "2,3", "1,2"}},
		{x: 8, y: 8, expect: []string{"8,7", "7,8"}},
	}

	// Create a 9x9 map with labeled tiles
	m := NewGrid(9, 9)
	m.Each(func(p Point, tile Tile[string]) {
		m.WriteAt(p.X, p.Y, Value(p.Integer()))
	})

	// Run all the tests
	for _, tc := range tests {
		var out []string
		m.Neighbors(tc.x, tc.y, func(_ Point, tile Tile[string]) {
			loc := unpackPoint(uint32(tile.Value()))
			out = append(out, loc.String())
		})
		assert.ElementsMatch(t, tc.expect, out)
	}
}

func TestAt(t *testing.T) {

	// Create a 9x9 map with labeled tiles
	m := NewGrid(9, 9)
	m.Each(func(p Point, tile Tile[string]) {
		m.WriteAt(p.X, p.Y, Value(p.Integer()))
	})

	// Make sure our At() and the position matches
	m.Each(func(p Point, tile Tile[string]) {
		at, _ := m.At(p.X, p.Y)
		assert.Equal(t, p.String(), unpackPoint(uint32(at.Value())).String())
	})

	// Make sure that points match
	for y := int16(0); y < 9; y++ {
		for x := int16(0); x < 9; x++ {
			at, _ := m.At(x, y)
			assert.Equal(t, At(x, y).String(), unpackPoint(uint32(at.Value())).String())
		}
	}
}

func TestUpdate(t *testing.T) {

	// Create a 9x9 map with labeled tiles
	m := NewGrid(9, 9)
	i := 0
	m.Each(func(p Point, _ Tile[string]) {
		i++
		m.WriteAt(p.X, p.Y, Value(i))
	})

	// Assert the update
	cursor, _ := m.At(8, 8)
	assert.Equal(t, 81, int(cursor.Value()))

	// 81 = 0b01010001
	delta := Value(0b00101110) // change last 2 bits and should ignore other bits
	m.MaskAt(8, 8, delta, Value(0b00000011))

	// original: 0101 0001
	// delta:    0010 1110
	// mask:     0000 0011
	// result:   0101 0010
	cursor, _ = m.At(8, 8)
	assert.Equal(t, 0b01010010, int(cursor.Value()))
}

func TestState(t *testing.T) {
	m := NewGrid(9, 9)
	m.Each(func(p Point, c Tile[string]) {
		c.Add(p.String())
		c.Add(p.String()) // duplicate
	})

	m.Each(func(p Point, c Tile[string]) {
		assert.Equal(t, 1, c.Count())
		assert.NoError(t, c.Range(func(s string) error {
			assert.Equal(t, p.String(), s)
			return nil
		}))

		c.Del(p.String())
		assert.Equal(t, 0, c.Count())
	})
}

func TestStateRangeErr(t *testing.T) {
	m := NewGrid(9, 9)
	m.Each(func(p Point, c Tile[string]) {
		c.Add(p.String())
	})

	m.Each(func(p Point, c Tile[string]) {
		assert.Error(t, c.Range(func(s string) error {
			return io.EOF
		}))
	})
}

func TestPointOf(t *testing.T) {
	truthTable := func(x, y int16, idx uint8) (int16, int16) {
		switch idx {
		case 0:
			return x, y
		case 1:
			return x + 1, y
		case 2:
			return x + 2, y
		case 3:
			return x, y + 1
		case 4:
			return x + 1, y + 1
		case 5:
			return x + 2, y + 1
		case 6:
			return x, y + 2
		case 7:
			return x + 1, y + 2
		case 8:
			return x + 2, y + 2
		default:
			return x, y
		}
	}

	for i := 0; i < 9; i++ {
		at := pointOf(At(0, 0), uint8(i))
		x, y := truthTable(0, 0, uint8(i))
		assert.Equal(t, x, at.X, fmt.Sprintf("idx=%v", i))
		assert.Equal(t, y, at.Y, fmt.Sprintf("idx=%v", i))
	}
}

func TestConcurrentMerge(t *testing.T) {
	const count = 10000
	var wg sync.WaitGroup
	wg.Add(count)

	m := NewGrid(9, 9)
	for i := 0; i < count; i++ {
		go func() {
			m.MergeAt(1, 1, func(v Value) Value {
				v += 1
				return v
			})
			wg.Done()
		}()
	}

	wg.Wait()
	tile, ok := m.At(1, 1)
	assert.True(t, ok)
	assert.Equal(t, uint32(count), tile.Value())
}


================================================
FILE: path.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"math"
	"math/bits"
	"sync"

	"github.com/kelindar/intmap"
)

type costFn = func(Value) uint16

// Edge represents an edge of the path
type edge struct {
	Point
	Cost uint32
}

// Around performs a breadth first search around a point.
func (m *Grid[T]) Around(from Point, distance uint32, costOf costFn, fn func(Point, Tile[T])) {
	start, ok := m.At(from.X, from.Y)
	if !ok {
		return
	}

	fn(from, start)

	// For pre-allocating, we use πr2 since BFS will result in a approximation
	// of a circle, in the worst case.
	maxArea := int(math.Ceil(math.Pi * float64(distance*distance)))

	// Acquire a frontier heap for search
	state := acquire(maxArea)
	frontier := state.frontier
	reached := state.edges
	defer release(state)

	frontier.Push(from.Integer(), 0)
	reached.Store(from.Integer(), 0)
	for !frontier.IsEmpty() {
		pCurr := frontier.Pop()
		current := unpackPoint(pCurr)

		// Get all of the neighbors
		m.Neighbors(current.X, current.Y, func(next Point, nextTile Tile[T]) {
			if d := from.DistanceTo(next); d > distance {
				return // Too far
			}

			if cost := costOf(nextTile.Value()); cost == 0 {
				return // Blocked tile, ignore completely
			}

			// Add to the search queue
			pNext := next.Integer()
			if _, ok := reached.Load(pNext); !ok {
				frontier.Push(pNext, 1)
				reached.Store(pNext, 1)
				fn(next, nextTile)
			}
		})
	}
}

// Path calculates a short path and the distance between the two locations
func (m *Grid[T]) Path(from, to Point, costOf costFn) ([]Point, int, bool) {
	distance := float64(from.DistanceTo(to))
	maxArea := int(math.Ceil(math.Pi * float64(distance*distance)))

	// For pre-allocating, we use πr2 since BFS will result in a approximation
	// of a circle, in the worst case.
	state := acquire(maxArea)
	edges := state.edges
	frontier := state.frontier
	defer release(state)

	frontier.Push(from.Integer(), 0)
	edges.Store(from.Integer(), encode(0, Direction(0))) // Starting point has no direction

	for !frontier.IsEmpty() {
		pCurr := frontier.Pop()
		current := unpackPoint(pCurr)

		// Decode the cost to reach the current point
		currentEncoded, _ := edges.Load(pCurr)
		currentCost, _ := decode(currentEncoded)

		// Check if we've reached the destination
		if current.Equal(to) {

			// Reconstruct the path
			path := make([]Point, 0, 64)
			path = append(path, current)
			for !current.Equal(from) {
				currentEncoded, _ := edges.Load(current.Integer())
				_, dir := decode(currentEncoded)
				current = current.Move(oppositeDirection(dir))
				path = append(path, current)
			}

			// Reverse the path to get from source to destination
			for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
				path[i], path[j] = path[j], path[i]
			}

			return path, int(currentCost), true
		}

		// Explore neighbors
		m.Neighbors(current.X, current.Y, func(next Point, nextTile Tile[T]) {
			cNext := costOf(nextTile.Value())
			if cNext == 0 {
				return // Blocked tile
			}

			nextCost := currentCost + uint32(cNext)
			pNext := next.Integer()

			existingEncoded, visited := edges.Load(pNext)
			existingCost, _ := decode(existingEncoded)

			// If we haven't visited this node or we found a better path
			if !visited || nextCost < existingCost {
				angle := angleOf(current, next)
				priority := nextCost + next.DistanceTo(to)

				// Store the edge and push to the frontier
				edges.Store(pNext, encode(nextCost, angle))
				frontier.Push(pNext, priority)
			}
		})
	}

	return nil, 0, false
}

// encode packs the cost and direction into a uint32
func encode(cost uint32, dir Direction) uint32 {
	return (cost << 4) | uint32(dir&0xF)
}

// decode unpacks the cost and direction from a uint32
func decode(value uint32) (cost uint32, dir Direction) {
	cost = value >> 4
	dir = Direction(value & 0xF)
	return
}

// -----------------------------------------------------------------------------

type pathfinder struct {
	edges    *intmap.Map
	frontier *frontier
}

var pathfinders = sync.Pool{
	New: func() any {
		return &pathfinder{
			edges:    intmap.NewWithFill(32, .99),
			frontier: newFrontier(),
		}
	},
}

// Acquires a new instance of a pathfinding state
func acquire(capacity int) *pathfinder {
	v := pathfinders.Get().(*pathfinder)
	if v.edges.Capacity() < capacity {
		v.edges = intmap.NewWithFill(capacity, .99)
	}

	return v
}

// release releases a pathfinding state back to the pool
func release(v *pathfinder) {
	v.edges.Clear()
	v.frontier.Reset()
	pathfinders.Put(v)
}

// -----------------------------------------------------------------------------

// frontier is a priority queue implementation that uses buckets to store
// elements. Original implementation by Iskander Sharipov (https://github.com/quasilyte/pathing)
type frontier struct {
	buckets [64][]uint32
	mask    uint64
}

// newFrontier creates a new frontier priority queue
func newFrontier() *frontier {
	h := &frontier{}
	for i := range &h.buckets {
		h.buckets[i] = make([]uint32, 0, 16)
	}
	return h
}

func (q *frontier) Reset() {
	buckets := &q.buckets

	// Reslice storage slices back.
	// To avoid traversing all len(q.buckets),
	// we have some offset to skip uninteresting (already empty) buckets.
	// We also stop when mask is 0 meaning all remaining buckets are empty too.
	// In other words, it would only touch slices between min and max non-empty priorities.
	mask := q.mask
	offset := uint(bits.TrailingZeros64(mask))
	mask >>= offset
	i := offset
	for mask != 0 {
		if i < uint(len(buckets)) {
			buckets[i] = buckets[i][:0]
		}
		mask >>= 1
		i++
	}

	q.mask = 0
}

func (q *frontier) IsEmpty() bool {
	return q.mask == 0
}

func (q *frontier) Push(value, priority uint32) {
	// No bound checks since compiler knows that i will never exceed 64.
	// We also get a cool truncation of values above 64 to store them
	// in our biggest bucket.
	i := priority & 0b111111
	q.buckets[i] = append(q.buckets[i], value)
	q.mask |= 1 << i
}

func (q *frontier) Pop() uint32 {
	buckets := &q.buckets

	// Using uints here and explicit len check to avoid the
	// implicitly inserted bound check.
	i := uint(bits.TrailingZeros64(q.mask))
	if i < uint(len(buckets)) {
		e := buckets[i][len(buckets[i])-1]
		buckets[i] = buckets[i][:len(buckets[i])-1]
		if len(buckets[i]) == 0 {
			q.mask &^= 1 << i
		}
		return e
	}

	// A queue is empty
	return 0
}


================================================
FILE: path_test.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"fmt"
	"image"
	"image/color"
	"image/png"
	"os"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPath(t *testing.T) {
	m := mapFrom("9x9.png")
	path, dist, found := m.Path(At(1, 1), At(7, 7), costOf)
	assert.Equal(t, `
.........
.x  .   .
.x ... ..
.xxx . ..
...x .  .
.  xxx  .
.....x...
.    xxx.
.........`, plotPath(m, path))

	fmt.Println(plotPath(m, path))
	assert.Equal(t, 12, dist)
	assert.True(t, found)
}

func TestPathTiny(t *testing.T) {
	m := NewGrid(6, 6)
	path, dist, found := m.Path(At(0, 0), At(5, 5), costOf)
	assert.Equal(t, `
x     
x     
x     
x     
x     
xxxxxx`, plotPath(m, path))
	assert.Equal(t, 10, dist)
	assert.True(t, found)
}

func TestDraw(t *testing.T) {
	m := mapFrom("9x9.png")
	out := drawGrid(m, NewRect(0, 0, 0, 0))
	assert.NotNil(t, out)
}

/*
BenchmarkPath/9x9-24         	 2856020	       423.0 ns/op	     256 B/op	       1 allocs/op
BenchmarkPath/300x300-24     	    1167	   1006143 ns/op	    3845 B/op	       4 allocs/op
BenchmarkPath/381x381-24     	    3150	    371478 ns/op	   12629 B/op	       5 allocs/op
BenchmarkPath/384x384-24     	    3178	    374982 ns/op	    7298 B/op	       5 allocs/op
BenchmarkPath/3069x3069-24   	     787	   1459683 ns/op	  106188 B/op	       7 allocs/op
BenchmarkPath/3072x3072-24   	     799	   1552230 ns/op	  104906 B/op	       7 allocs/op
BenchmarkPath/6144x6144-24   	    3099	    381935 ns/op	   12716 B/op	       5 allocs/op
*/
func BenchmarkPath(b *testing.B) {
	b.Run("9x9", func(b *testing.B) {
		m := mapFrom("9x9.png")
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Path(At(1, 1), At(7, 7), costOf)
		}
	})

	b.Run("300x300", func(b *testing.B) {
		m := mapFrom("300x300.png")
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Path(At(115, 20), At(160, 270), costOf)
		}
	})

	b.Run("381x381", func(b *testing.B) {
		m := NewGrid(381, 381)
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Path(At(0, 0), At(380, 380), costOf)
		}
	})

	b.Run("384x384", func(b *testing.B) {
		m := NewGrid(384, 384)
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Path(At(0, 0), At(380, 380), costOf)
		}
	})

	b.Run("3069x3069", func(b *testing.B) {
		m := NewGrid(3069, 3069)
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Path(At(0, 0), At(700, 700), costOf)
		}
	})

	b.Run("3072x3072", func(b *testing.B) {
		m := NewGrid(3072, 3072)
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Path(At(0, 0), At(700, 700), costOf)
		}
	})

	b.Run("6144x6144", func(b *testing.B) {
		m := NewGrid(6144, 6144)
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Path(At(0, 0), At(380, 380), costOf)
		}
	})
}

/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkAround/3r-24 	 2080566	     562.7 ns/op	       0 B/op	       0 allocs/op
BenchmarkAround/5r-24 	  885582	      1358 ns/op	       0 B/op	       0 allocs/op
BenchmarkAround/10r-24    300672	      3953 ns/op	       0 B/op	       0 allocs/op
*/
func BenchmarkAround(b *testing.B) {
	m := mapFrom("300x300.png")
	b.Run("3r", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Around(At(115, 20), 3, costOf, func(_ Point, _ Tile[string]) {})
		}
	})

	b.Run("5r", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Around(At(115, 20), 5, costOf, func(_ Point, _ Tile[string]) {})
		}
	})

	b.Run("10r", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			m.Around(At(115, 20), 10, costOf, func(_ Point, _ Tile[string]) {})
		}
	})
}

func TestAround(t *testing.T) {
	m := mapFrom("9x9.png")

	for i := 0; i < 3; i++ {
		var path []string
		m.Around(At(2, 2), 3, costOf, func(p Point, tile Tile[string]) {
			path = append(path, p.String())
		})
		assert.Equal(t, 10, len(path))
		assert.ElementsMatch(t, []string{
			"2,2", "2,1", "2,3", "1,2", "3,1",
			"1,1", "1,3", "3,3", "4,3", "3,4",
		}, path)
	}
}

func TestAroundMiss(t *testing.T) {
	m := mapFrom("9x9.png")
	m.Around(At(20, 20), 3, costOf, func(p Point, tile Tile[string]) {
		t.Fail()
	})
}

/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkHeap-24    	  240228	      5076 ns/op	    6016 B/op	      68 allocs/op
*/
func BenchmarkHeap(b *testing.B) {
	for i := 0; i < b.N; i++ {
		h := newFrontier()
		for j := 0; j < 128; j++ {
			h.Push(rand(j), 1)
		}
		for j := 0; j < 128*10; j++ {
			h.Push(rand(j), 1)
			h.Pop()
		}
	}
}

// very fast semi-random function
func rand(i int) uint32 {
	i = i + 10000
	i = i ^ (i << 16)
	i = (i >> 5) ^ i
	return uint32(i & 0xFF)
}

// -----------------------------------------------------------------------------

// Cost estimation function
func costOf(tile Value) uint16 {
	if (tile)&1 != 0 {
		return 0 // Blocked
	}
	return 1
}

// mapFrom creates a map from ASCII string
func mapFrom(name string) *Grid[string] {
	f, err := os.Open("fixtures/" + name)
	defer f.Close()
	if err != nil {
		panic(err)
	}

	// Decode the image
	img, err := png.Decode(f)
	if err != nil {
		panic(err)
	}

	m := NewGrid(int16(img.Bounds().Dx()), int16(img.Bounds().Dy()))
	for y := int16(0); y < m.Size.Y; y++ {
		for x := int16(0); x < m.Size.X; x++ {
			//fmt.Printf("%+v %T\n", img.At(int(x), int(y)), img.At(int(x), int(y)))
			v := img.At(int(x), int(y)).(color.RGBA)
			switch v.R {
			case 255:
			case 0:
				m.WriteAt(x, y, Value(0xff))
			}

		}
	}
	return m
}

// plotPath plots the path on ASCII map
func plotPath(m *Grid[string], path []Point) string {
	out := make([][]byte, m.Size.Y)
	for i := range out {
		out[i] = make([]byte, m.Size.X)
	}

	m.Each(func(l Point, tile Tile[string]) {
		//println(l.String(), int(tile[0]))
		switch {
		case pointInPath(l, path):
			out[l.Y][l.X] = 'x'
		case tile.Value()&1 != 0:
			out[l.Y][l.X] = '.'
		default:
			out[l.Y][l.X] = ' '
		}
	})

	var sb strings.Builder
	for _, line := range out {
		sb.WriteByte('\n')
		sb.WriteString(string(line))
	}
	return sb.String()
}

// pointInPath returns whether a point is part of a path or not
func pointInPath(point Point, path []Point) bool {
	for _, p := range path {
		if p.Equal(point) {
			return true
		}
	}
	return false
}

// draw converts the map to a black and white image for debugging purposes.
func drawGrid(m *Grid[string], rect Rect) image.Image {
	if rect.Max.X == 0 || rect.Max.Y == 0 {
		rect = NewRect(0, 0, m.Size.X, m.Size.Y)
	}

	size := rect.Size()
	output := image.NewRGBA(image.Rect(0, 0, int(size.X), int(size.Y)))
	m.Within(rect.Min, rect.Max, func(p Point, tile Tile[string]) {
		a := uint8(255)
		if tile.Value() == 1 {
			a = 0
		}

		output.SetRGBA(int(p.X), int(p.Y), color.RGBA{a, a, a, 255})
	})
	return output
}


================================================
FILE: point.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"fmt"
	"math"
)

// -----------------------------------------------------------------------------

// Point represents a 2D coordinate.
type Point struct {
	X int16 // X coordinate
	Y int16 // Y coordinate
}

func unpackPoint(v uint32) Point {
	return At(int16(v>>16), int16(v))
}

// At creates a new point at a specified x,y coordinate.
func At(x, y int16) Point {
	return Point{X: x, Y: y}
}

// String returns string representation of a point.
func (p Point) String() string {
	return fmt.Sprintf("%v,%v", p.X, p.Y)
}

// Integer returns a packed 32-bit integer representation of a point.
func (p Point) Integer() uint32 {
	return (uint32(p.X) << 16) | (uint32(p.Y) & 0xffff)
}

// Equal compares two points and returns true if they are equal.
func (p Point) Equal(other Point) bool {
	return p.X == other.X && p.Y == other.Y
}

// Add adds two points together.
func (p Point) Add(p2 Point) Point {
	return Point{p.X + p2.X, p.Y + p2.Y}
}

// Subtract subtracts the second point from the first.
func (p Point) Subtract(p2 Point) Point {
	return Point{p.X - p2.X, p.Y - p2.Y}
}

// Multiply multiplies two points together.
func (p Point) Multiply(p2 Point) Point {
	return Point{p.X * p2.X, p.Y * p2.Y}
}

// Divide divides the first point by the second.
func (p Point) Divide(p2 Point) Point {
	return Point{p.X / p2.X, p.Y / p2.Y}
}

// MultiplyScalar multiplies the given point by the scalar.
func (p Point) MultiplyScalar(s int16) Point {
	return Point{p.X * s, p.Y * s}
}

// DivideScalar divides the given point by the scalar.
func (p Point) DivideScalar(s int16) Point {
	return Point{p.X / s, p.Y / s}
}

// Within checks if the point is within the specified bounding box.
func (p Point) Within(nw, se Point) bool {
	return Rect{Min: nw, Max: se}.Contains(p)
}

// WithinRect checks if the point is within the specified bounding box.
func (p Point) WithinRect(box Rect) bool {
	return box.Contains(p)
}

// WithinSize checks if the point is within the specified bounding box
// which starts at 0,0 until the width/height provided.
func (p Point) WithinSize(size Point) bool {
	return p.X >= 0 && p.Y >= 0 && p.X < size.X && p.Y < size.Y
}

// Move moves a point by one in the specified direction.
func (p Point) Move(direction Direction) Point {
	return p.MoveBy(direction, 1)
}

// MoveBy moves a point by n in the specified direction.
func (p Point) MoveBy(direction Direction, n int16) Point {
	switch direction {
	case North:
		return Point{p.X, p.Y - n}
	case NorthEast:
		return Point{p.X + n, p.Y - n}
	case East:
		return Point{p.X + n, p.Y}
	case SouthEast:
		return Point{p.X + n, p.Y + n}
	case South:
		return Point{p.X, p.Y + n}
	case SouthWest:
		return Point{p.X - n, p.Y + n}
	case West:
		return Point{p.X - n, p.Y}
	case NorthWest:
		return Point{p.X - n, p.Y - n}
	default:
		return p
	}
}

// DistanceTo calculates manhattan distance to the other point
func (p Point) DistanceTo(other Point) uint32 {
	return abs(int32(p.X)-int32(other.X)) + abs(int32(p.Y)-int32(other.Y))
}

// Angle calculates the angle between two points
func (p Point) Angle(other Point) Direction {
	dx := float64(other.X - p.X)
	dy := float64(other.Y - p.Y)

	// Calculate the angle in radians
	angle := math.Atan2(dy, dx)
	alpha := angle + math.Pi/2
	if alpha < 0 {
		alpha += 2 * math.Pi
	}

	// Map to 8 directions (0-7)
	return Direction(math.Round(alpha/(math.Pi/4))) % 8
}

func abs(n int32) uint32 {
	if n < 0 {
		return uint32(-n)
	}
	return uint32(n)
}

// -----------------------------------------------------------------------------

// Rect represents a rectangle
type Rect struct {
	Min Point // Top left point of the rectangle
	Max Point // Bottom right point of the rectangle
}

// NewRect creates a new rectangle
// left,top,right,bottom correspond to x1,y1,x2,y2
func NewRect(left, top, right, bottom int16) Rect {
	return Rect{Min: At(left, top), Max: At(right, bottom)}
}

// Contains returns whether a point is within the rectangle or not.
func (a Rect) Contains(p Point) bool {
	return a.Min.X <= p.X && p.X < a.Max.X && a.Min.Y <= p.Y && p.Y < a.Max.Y
}

// Intersects returns whether a rectangle intersects with another rectangle or not.
func (a Rect) Intersects(b Rect) bool {
	return b.Min.X < a.Max.X && a.Min.X < b.Max.X && b.Min.Y < a.Max.Y && a.Min.Y < b.Max.Y
}

// Size returns the size of the rectangle
func (a *Rect) Size() Point {
	return Point{
		X: a.Max.X - a.Min.X,
		Y: a.Max.Y - a.Min.Y,
	}
}

// IsZero returns true if the rectangle is zero-value
func (a Rect) IsZero() bool {
	return a.Min.X == a.Max.X && a.Min.Y == a.Max.Y
}

// Difference calculates up to four non-overlapping regions in a that are not covered by b.
// If there are fewer than four distinct regions, the remaining Rects will be zero-value.
func (a Rect) Difference(b Rect) (result [4]Rect) {
	if b.Contains(a.Min) && b.Contains(a.Max) {
		return // Fully covered, return zero-value result
	}

	// Check for non-overlapping cases
	if !a.Intersects(b) {
		result[0] = a // No overlap, return A as is
		return
	}

	left := min(a.Min.X, b.Min.X)
	right := max(a.Max.X, b.Max.X)
	top := min(a.Min.Y, b.Min.Y)
	bottom := max(a.Max.Y, b.Max.Y)

	result[0].Min = Point{X: left, Y: top}
	result[0].Max = Point{X: right, Y: max(a.Min.Y, b.Min.Y)}

	result[1].Min = Point{X: left, Y: min(a.Max.Y, b.Max.Y)}
	result[1].Max = Point{X: right, Y: bottom}

	result[2].Min = Point{X: left, Y: top}
	result[2].Max = Point{X: max(a.Min.X, b.Min.X), Y: bottom}

	result[3].Min = Point{X: min(a.Max.X, b.Max.X), Y: top}
	result[3].Max = Point{X: right, Y: bottom}

	if result[0].Size().X == 0 || result[0].Size().Y == 0 {
		result[0] = Rect{}
	}
	if result[1].Size().X == 0 || result[1].Size().Y == 0 {
		result[1] = Rect{}
	}
	if result[2].Size().X == 0 || result[2].Size().Y == 0 {
		result[2] = Rect{}
	}
	if result[3].Size().X == 0 || result[3].Size().Y == 0 {
		result[3] = Rect{}
	}

	return
}

// Pack returns a packed representation of a rectangle
func (a Rect) pack() uint64 {
	return uint64(a.Min.Integer())<<32 | uint64(a.Max.Integer())
}

// Unpack returns a rectangle from a packed representation
func unpackRect(v uint64) Rect {
	return Rect{
		Min: unpackPoint(uint32(v >> 32)),
		Max: unpackPoint(uint32(v)),
	}
}

// -----------------------------------------------------------------------------

// Diretion represents a direction
type Direction byte

// Various directions
const (
	North Direction = iota
	NorthEast
	East
	SouthEast
	South
	SouthWest
	West
	NorthWest
)

// String returns a string representation of a direction
func (v Direction) String() string {
	switch v {
	case North:
		return "🡱N"
	case NorthEast:
		return "🡵NE"
	case East:
		return "🡲E"
	case SouthEast:
		return "🡶SE"
	case South:
		return "🡳S"
	case SouthWest:
		return "🡷SW"
	case West:
		return "🡰W"
	case NorthWest:
		return "🡴NW"
	default:
		return ""
	}
}

// Vector returns a direction vector with a given scale
func (v Direction) Vector(scale int16) Point {
	return Point{}.MoveBy(v, scale)
}

// angleOf returns the direction from one point to another
func angleOf(from, to Point) Direction {
	dx := to.X - from.X
	dy := to.Y - from.Y

	switch {
	case dx == 0 && dy == -1:
		return North
	case dx == 1 && dy == -1:
		return NorthEast
	case dx == 1 && dy == 0:
		return East
	case dx == 1 && dy == 1:
		return SouthEast
	case dx == 0 && dy == 1:
		return South
	case dx == -1 && dy == 1:
		return SouthWest
	case dx == -1 && dy == 0:
		return West
	case dx == -1 && dy == -1:
		return NorthWest
	default:
		return Direction(0) // Invalid direction
	}
}

// oppositeDirection returns the opposite of the given direction
func oppositeDirection(dir Direction) Direction {
	return Direction((dir + 4) % 8)
}


================================================
FILE: point_test.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkPoint/within-24         	1000000000	         0.09854 ns/op	       0 B/op	       0 allocs/op
BenchmarkPoint/within-rect-24    	1000000000	         0.09966 ns/op	       0 B/op	       0 allocs/op
*/
func BenchmarkPoint(b *testing.B) {
	p := At(10, 20)
	b.Run("within", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			p.Within(At(0, 0), At(100, 100))
		}
	})

	b.Run("within-rect", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			p.WithinRect(NewRect(0, 0, 100, 100))
		}
	})
}

func TestPoint(t *testing.T) {
	p := At(10, 20)
	p2 := At(2, 2)

	assert.Equal(t, int16(10), p.X)
	assert.Equal(t, int16(20), p.Y)
	assert.Equal(t, uint32(0xa0014), p.Integer())
	assert.Equal(t, At(-5, 5), unpackPoint(At(-5, 5).Integer()))
	assert.Equal(t, "10,20", p.String())
	assert.True(t, p.Equal(At(10, 20)))
	assert.Equal(t, "20,40", p.MultiplyScalar(2).String())
	assert.Equal(t, "5,10", p.DivideScalar(2).String())
	assert.Equal(t, "12,22", p.Add(p2).String())
	assert.Equal(t, "8,18", p.Subtract(p2).String())
	assert.Equal(t, "20,40", p.Multiply(p2).String())
	assert.Equal(t, "5,10", p.Divide(p2).String())
	assert.True(t, p.Within(At(1, 1), At(20, 30)))
	assert.True(t, p.WithinRect(NewRect(1, 1, 20, 30)))
	assert.False(t, p.WithinSize(At(10, 20)))
	assert.True(t, p.WithinSize(At(20, 30)))
}

func TestIntersects(t *testing.T) {
	assert.True(t, NewRect(0, 0, 2, 2).Intersects(NewRect(1, 0, 3, 2)))
	assert.False(t, NewRect(0, 0, 2, 2).Intersects(NewRect(2, 0, 4, 2)))
	assert.False(t, NewRect(10, 10, 12, 12).Intersects(NewRect(9, 12, 11, 14)))
}

func TestDirection(t *testing.T) {
	for i := 0; i < 8; i++ {
		dir := Direction(i)
		assert.NotEmpty(t, dir.String())
	}
}

func TestDirection_Empty(t *testing.T) {
	dir := Direction(9)
	assert.Empty(t, dir.String())
}

func TestPointAngle(t *testing.T) {
	tests := []struct {
		name     string
		from     Point
		to       Point
		expected Direction
	}{
		// Cardinal directions from origin
		{"North", At(0, 0), At(0, -1), North},
		{"East", At(0, 0), At(1, 0), East},
		{"South", At(0, 0), At(0, 1), South},
		{"West", At(0, 0), At(-1, 0), West},

		// Diagonal directions from origin
		{"NorthEast", At(0, 0), At(1, -1), NorthEast},
		{"SouthEast", At(0, 0), At(1, 1), SouthEast},
		{"SouthWest", At(0, 0), At(-1, 1), SouthWest},
		{"NorthWest", At(0, 0), At(-1, -1), NorthWest},

		// Same point (math.Atan2(0,0) = 0, which maps to East after transformation)
		{"Same point", At(5, 5), At(5, 5), East},

		// Non-origin starting points
		{"From 10,10 North", At(10, 10), At(10, 5), North},
		{"From 10,10 East", At(10, 10), At(15, 10), East},
		{"From 10,10 South", At(10, 10), At(10, 15), South},
		{"From 10,10 West", At(10, 10), At(5, 10), West},
		{"From 10,10 NorthEast", At(10, 10), At(15, 5), NorthEast},
		{"From 10,10 SouthEast", At(10, 10), At(15, 15), SouthEast},
		{"From 10,10 SouthWest", At(10, 10), At(5, 15), SouthWest},
		{"From 10,10 NorthWest", At(10, 10), At(5, 5), NorthWest},

		// Edge cases with larger distances
		{"Far North", At(0, 0), At(0, -100), North},
		{"Far East", At(0, 0), At(100, 0), East},
		{"Far South", At(0, 0), At(0, 100), South},
		{"Far West", At(0, 0), At(-100, 0), West},

		// Angles close to boundaries (testing rounding)
		{"Near North boundary", At(0, 0), At(1, -10), North},
		{"Near NorthEast boundary", At(0, 0), At(10, -10), NorthEast},
		{"Near East boundary", At(0, 0), At(10, -1), East},
		{"Near SouthEast boundary", At(0, 0), At(10, 10), SouthEast},

		// Negative coordinates
		{"Negative coords North", At(-5, -5), At(-5, -10), North},
		{"Negative coords East", At(-5, -5), At(0, -5), East},
		{"Negative coords South", At(-5, -5), At(-5, 0), South},
		{"Negative coords West", At(-5, -5), At(-10, -5), West},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := tc.from.Angle(tc.to)
			assert.Equal(t, tc.expected, result,
				"Point %s to %s should be %s, got %s",
				tc.from.String(), tc.to.String(), tc.expected.String(), result.String())
		})
	}
}

func TestMove(t *testing.T) {
	tests := []struct {
		dir Direction
		out Point
	}{
		{North, Point{X: 0, Y: -1}},
		{South, Point{X: 0, Y: 1}},
		{East, Point{X: 1, Y: 0}},
		{West, Point{X: -1, Y: 0}},
		{NorthEast, Point{X: 1, Y: -1}},
		{NorthWest, Point{X: -1, Y: -1}},
		{SouthEast, Point{X: 1, Y: 1}},
		{SouthWest, Point{X: -1, Y: 1}},
		{Direction(99), Point{}},
	}

	for _, tc := range tests {
		assert.Equal(t, tc.out, Point{}.Move(tc.dir), tc.dir.String())
	}
}

func TestContains(t *testing.T) {
	tests := map[Point]bool{
		{X: 0, Y: 0}: true,
		{X: 1, Y: 0}: true,
		{X: 0, Y: 1}: true,
		{X: 1, Y: 1}: true,
		{X: 2, Y: 2}: false,
		{X: 3, Y: 3}: false,
		{X: 1, Y: 2}: false,
		{X: 2, Y: 1}: false,
	}

	for point, expect := range tests {
		r := NewRect(0, 0, 2, 2)
		assert.Equal(t, expect, r.Contains(point), point.String())
	}
}

func TestDiff_Right(t *testing.T) {
	a := Rect{At(0, 0), At(2, 2)}
	b := Rect{At(1, 0), At(3, 2)}

	diff := a.Difference(b)
	assert.Equal(t, Rect{At(0, 0), At(1, 2)}, diff[2])
	assert.Equal(t, Rect{At(2, 0), At(3, 2)}, diff[3])
}

func TestDiff_Left(t *testing.T) {
	a := Rect{At(0, 0), At(2, 2)}
	b := Rect{At(-1, 0), At(1, 2)}

	diff := a.Difference(b)
	assert.Equal(t, Rect{At(-1, 0), At(0, 2)}, diff[2])
	assert.Equal(t, Rect{At(1, 0), At(2, 2)}, diff[3])
}

func TestDiff_Up(t *testing.T) {
	a := Rect{At(0, 0), At(2, 2)}
	b := Rect{At(0, -1), At(2, 1)}

	diff := a.Difference(b)
	assert.Equal(t, Rect{At(0, -1), At(2, 0)}, diff[0])
	assert.Equal(t, Rect{At(0, 1), At(2, 2)}, diff[1])
}

func TestDiff_Down(t *testing.T) {
	a := Rect{At(0, 0), At(2, 2)}
	b := Rect{At(0, 1), At(2, 3)}

	diff := a.Difference(b)
	assert.Equal(t, Rect{At(0, 0), At(2, 1)}, diff[0])
	assert.Equal(t, Rect{At(0, 2), At(2, 3)}, diff[1])
}


================================================
FILE: store.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"compress/flate"
	"encoding/binary"
	"io"
	"os"
	"unsafe"

	"github.com/kelindar/iostream"
)

const tileDataSize = int(unsafe.Sizeof([9]Value{}))

// ---------------------------------- Stream ----------------------------------

// WriteTo writes the grid to a specific writer.
func (m *Grid[T]) WriteTo(dst io.Writer) (n int64, err error) {
	p1 := At(0, 0)
	p2 := At(m.Size.X-1, m.Size.Y-1)

	// Write the viewport size
	w := iostream.NewWriter(dst)
	header := make([]byte, 8)
	binary.BigEndian.PutUint16(header[0:2], uint16(p1.X))
	binary.BigEndian.PutUint16(header[2:4], uint16(p1.Y))
	binary.BigEndian.PutUint16(header[4:6], uint16(p2.X))
	binary.BigEndian.PutUint16(header[6:8], uint16(p2.Y))
	if _, err := w.Write(header); err != nil {
		return w.Offset(), err
	}

	// Write the grid data
	m.pagesWithin(p1, p2, func(page *page[T]) {
		buffer := (*[tileDataSize]byte)(unsafe.Pointer(&page.tiles))[:]
		if _, err := w.Write(buffer); err != nil {
			return
		}
	})
	return w.Offset(), nil
}

// ReadFrom reads the grid from the reader.
func ReadFrom[T comparable](src io.Reader) (grid *Grid[T], err error) {
	r := iostream.NewReader(src)
	header := make([]byte, 8)
	if _, err := io.ReadFull(r, header); err != nil {
		return nil, err
	}

	// Read the size
	var view Rect
	view.Min.X = int16(binary.BigEndian.Uint16(header[0:2]))
	view.Min.Y = int16(binary.BigEndian.Uint16(header[2:4]))
	view.Max.X = int16(binary.BigEndian.Uint16(header[4:6]))
	view.Max.Y = int16(binary.BigEndian.Uint16(header[6:8]))

	// Allocate a new grid
	grid = NewGridOf[T](view.Max.X+1, view.Max.Y+1)
	buf := make([]byte, tileDataSize)
	grid.pagesWithin(view.Min, view.Max, func(page *page[T]) {
		if _, err = io.ReadFull(r, buf); err != nil {
			return
		}

		copy((*[tileDataSize]byte)(unsafe.Pointer(&page.tiles))[:], buf)
	})
	return
}

// ---------------------------------- File ----------------------------------

// WriteFile writes the grid into a flate-compressed binary file.
func (m *Grid[T]) WriteFile(filename string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}

	defer file.Close()
	writer, err := flate.NewWriter(file, flate.BestSpeed)
	if err != nil {
		return err
	}

	// WriteTo the underlying writer
	defer writer.Close()
	_, err = m.WriteTo(writer)
	return err
}

// Restore restores the grid from the specified file. The grid must
// be written using the corresponding WriteFile() method.
func ReadFile[T comparable](filename string) (grid *Grid[T], err error) {
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return nil, os.ErrNotExist
	}

	// Otherwise, attempt to open the file and restore
	file, err := os.Open(filename)
	if err != nil {
		return nil, err
	}

	defer file.Close()
	return ReadFrom[T](flate.NewReader(file))
}


================================================
FILE: store_test.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"bytes"
	"compress/flate"
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
)

/*
cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkStore/save-8         	   14455	     81883 ns/op	       8 B/op	       1 allocs/op
BenchmarkStore/read-8         	    2787	    399699 ns/op	  647421 B/op	       7 allocs/op
*/
func BenchmarkStore(b *testing.B) {
	m := mapFrom("300x300.png")

	b.Run("save", func(b *testing.B) {
		out := bytes.NewBuffer(make([]byte, 0, 550000))

		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			out.Reset()
			m.WriteTo(out)
		}
	})

	b.Run("read", func(b *testing.B) {
		enc := new(bytes.Buffer)
		m.WriteTo(enc)

		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			ReadFrom[string](bytes.NewBuffer(enc.Bytes()))
		}
	})

}

func TestSaveLoad(t *testing.T) {
	m := mapFrom("300x300.png")

	// Save the map
	enc := new(bytes.Buffer)
	n, err := m.WriteTo(enc)
	assert.NoError(t, err)
	assert.Equal(t, int64(360008), n)

	// Load the map back
	out, err := ReadFrom[string](enc)
	assert.NoError(t, err)
	assert.Equal(t, m.pages, out.pages)
}

func TestSaveLoadFlate(t *testing.T) {
	m := mapFrom("300x300.png")

	// Save the map
	output := new(bytes.Buffer)
	writer, err := flate.NewWriter(output, flate.BestSpeed)
	assert.NoError(t, err)

	n, err := m.WriteTo(writer)
	assert.NoError(t, writer.Close())
	assert.NoError(t, err)
	assert.Equal(t, int64(360008), n)
	assert.Equal(t, int(16533), output.Len())

	// Load the map back
	reader := flate.NewReader(output)
	out, err := ReadFrom[string](reader)
	assert.NoError(t, err)
	assert.Equal(t, m.pages, out.pages)
}

func TestSaveLoadFile(t *testing.T) {
	temp, err := os.CreateTemp("", "*")
	assert.NoError(t, err)
	defer os.Remove(temp.Name())

	// Write a test map into temp file
	m := mapFrom("300x300.png")
	assert.NoError(t, m.WriteFile(temp.Name()))

	fi, _ := temp.Stat()
	assert.Equal(t, int64(16533), fi.Size())

	// Read the map back
	out, err := ReadFile[string](temp.Name())
	assert.NoError(t, err)
	assert.Equal(t, m.pages, out.pages)
}


================================================
FILE: view.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"sync"
	"sync/atomic"
)

// Observer represents a tile update Observer.
type Observer[T comparable] interface {
	Viewport() Rect
	Resize(Rect, func(Point, Tile[T]))
	onUpdate(*Update[T])
}

// ValueAt represents a tile and its value.
type ValueAt struct {
	Point // The point of the tile
	Value // The value of the tile
}

// Update represents a tile update notification.
type Update[T comparable] struct {
	Old ValueAt // Old tile + value
	New ValueAt // New tile + value
	Add T       // An object was added to the tile
	Del T       // An object was removed from the tile
}

var _ Observer[string] = (*View[string, string])(nil)

// View represents a view which can monitor a collection of tiles. Type parameters
// S and T are the state and tile types respectively.
type View[S any, T comparable] struct {
	Grid  *Grid[T]       // The associated map
	Inbox chan Update[T] // The update inbox for the view
	State S              // The state of the view
	rect  atomic.Uint64  // The view box
}

// NewView creates a new view for a map with a given state. State can be anything
// that is passed to the view and can be used to store additional information.
func NewView[S any, T comparable](m *Grid[T], state S) *View[S, T] {
	v := &View[S, T]{
		Grid:  m,
		Inbox: make(chan Update[T], 32),
		State: state,
	}
	v.rect.Store(NewRect(-1, -1, -1, -1).pack())
	return v
}

// Viewport returns the current viewport of the view.
func (v *View[S, T]) Viewport() Rect {
	return unpackRect(v.rect.Load())
}

// Resize resizes the viewport and notifies the observers of the changes.
func (v *View[S, T]) Resize(view Rect, fn func(Point, Tile[T])) {
	grid := v.Grid
	prev := unpackRect(v.rect.Swap(view.pack()))

	for _, diff := range view.Difference(prev) {
		if diff.IsZero() {
			continue // Skip zero-value rectangles
		}

		grid.pagesWithin(diff.Min, diff.Max, func(page *page[T]) {
			r := page.Bounds()
			switch {

			// Page is now in view
			case view.Intersects(r) && !prev.Intersects(r):
				if grid.observers.Subscribe(page.point, v) {
					page.SetObserved(true) // Mark the page as being observed
				}

			// Page is no longer in view
			case !view.Intersects(r) && prev.Intersects(r):
				if grid.observers.Unsubscribe(page.point, v) {
					page.SetObserved(false) // Mark the page as not being observed
				}
			}

			// Callback for each new tile in the view
			if fn != nil {
				page.Each(v.Grid, func(p Point, tile Tile[T]) {
					if view.Contains(p) && !prev.Contains(p) {
						fn(p, tile)
					}
				})
			}
		})
	}
}

// MoveTo moves the viewport towards a particular direction.
func (v *View[S, T]) MoveTo(angle Direction, distance int16, fn func(Point, Tile[T])) {
	p := angle.Vector(distance)
	r := v.Viewport()
	v.Resize(Rect{
		Min: r.Min.Add(p),
		Max: r.Max.Add(p),
	}, fn)
}

// MoveBy moves the viewport towards a particular direction.
func (v *View[S, T]) MoveBy(x, y int16, fn func(Point, Tile[T])) {
	r := v.Viewport()
	v.Resize(Rect{
		Min: r.Min.Add(At(x, y)),
		Max: r.Max.Add(At(x, y)),
	}, fn)
}

// MoveAt moves the viewport to a specific coordinate.
func (v *View[S, T]) MoveAt(nw Point, fn func(Point, Tile[T])) {
	r := v.Viewport()
	size := r.Max.Subtract(r.Min)
	v.Resize(Rect{
		Min: nw,
		Max: nw.Add(size),
	}, fn)
}

// Each iterates over all of the tiles in the view.
func (v *View[S, T]) Each(fn func(Point, Tile[T])) {
	r := v.Viewport()
	v.Grid.Within(r.Min, r.Max, fn)
}

// At returns the tile at a specified position.
func (v *View[S, T]) At(x, y int16) (Tile[T], bool) {
	return v.Grid.At(x, y)
}

// WriteAt updates the entire tile at a specific coordinate.
func (v *View[S, T]) WriteAt(x, y int16, tile Value) {
	v.Grid.WriteAt(x, y, tile)
}

// MergeAt updates the bits of tile at a specific coordinate. The bits are specified
// by the mask. The bits that need to be updated should be flipped on in the mask.
func (v *View[S, T]) MergeAt(x, y int16, tile, mask Value) {
	v.Grid.MaskAt(x, y, tile, mask)
}

// Close closes the view and unsubscribes from everything.
func (v *View[S, T]) Close() error {
	r := v.Viewport()
	v.Grid.pagesWithin(r.Min, r.Max, func(page *page[T]) {
		if v.Grid.observers.Unsubscribe(page.point, v) {
			page.SetObserved(false) // Mark the page as not being observed
		}
	})
	return nil
}

// onUpdate occurs when a tile has updated.
func (v *View[S, T]) onUpdate(ev *Update[T]) {
	v.Inbox <- *ev // (copy)
}

// -----------------------------------------------------------------------------

// Pubsub represents a publish/subscribe layer for observers.
type pubsub[T comparable] struct {
	m   sync.Map  // Concurrent map of observers
	tmp sync.Pool // Temporary observer sets for notifications
}

// Subscribe registers an event listener on a system
func (p *pubsub[T]) Subscribe(page Point, sub Observer[T]) bool {
	if v, ok := p.m.Load(page.Integer()); ok {
		return v.(*observers[T]).Subscribe(sub)
	}

	// Slow path
	v, _ := p.m.LoadOrStore(page.Integer(), newObservers[T]())
	return v.(*observers[T]).Subscribe(sub)
}

// Unsubscribe deregisters an event listener from a system
func (p *pubsub[T]) Unsubscribe(page Point, sub Observer[T]) bool {
	if v, ok := p.m.Load(page.Integer()); ok {
		return v.(*observers[T]).Unsubscribe(sub)
	}
	return false
}

// Notify notifies listeners of an update that happened.
func (p *pubsub[T]) Notify1(ev *Update[T], page Point) {
	p.Each1(func(sub Observer[T]) {
		viewport := sub.Viewport()
		if viewport.Contains(ev.New.Point) || viewport.Contains(ev.Old.Point) {
			sub.onUpdate(ev)
		}
	}, page)
}

// Notify notifies listeners of an update that happened.
func (p *pubsub[T]) Notify2(ev *Update[T], pages [2]Point) {
	p.Each2(func(sub Observer[T]) {
		viewport := sub.Viewport()
		if viewport.Contains(ev.New.Point) || viewport.Contains(ev.Old.Point) {
			sub.onUpdate(ev)
		}
	}, pages)
}

// Each iterates over each observer in a page
func (p *pubsub[T]) Each1(fn func(sub Observer[T]), page Point) {
	if v, ok := p.m.Load(page.Integer()); ok {
		v.(*observers[T]).Each(func(sub Observer[T]) {
			fn(sub)
		})
	}
}

// Each2 iterates over each observer in a page
func (p *pubsub[T]) Each2(fn func(sub Observer[T]), pages [2]Point) {
	targets := p.tmp.Get().(map[Observer[T]]struct{})
	clear(targets)
	defer p.tmp.Put(targets)

	// Collect all observers from all pages
	for _, page := range pages {
		if v, ok := p.m.Load(page.Integer()); ok {
			v.(*observers[T]).Each(func(sub Observer[T]) {
				targets[sub] = struct{}{}
			})
		}
	}

	// Invoke the callback for each observer, once
	for sub := range targets {
		fn(sub)
	}
}

// -----------------------------------------------------------------------------

// Observers represents a change notifier which notifies the subscribers when
// a specific tile is updated.
type observers[T comparable] struct {
	sync.Mutex
	subs []Observer[T]
}

// newObservers creates a new instance of an change observer.
func newObservers[T comparable]() *observers[T] {
	return &observers[T]{
		subs: make([]Observer[T], 0, 8),
	}
}

// Each iterates over each observer
func (s *observers[T]) Each(fn func(sub Observer[T])) {
	if s == nil {
		return
	}

	s.Lock()
	defer s.Unlock()
	for _, sub := range s.subs {
		fn(sub)
	}
}

// Subscribe registers an event listener on a system
func (s *observers[T]) Subscribe(sub Observer[T]) bool {
	s.Lock()
	defer s.Unlock()
	s.subs = append(s.subs, sub)
	return len(s.subs) > 0 // At least one
}

// Unsubscribe deregisters an event listener from a system
func (s *observers[T]) Unsubscribe(sub Observer[T]) bool {
	s.Lock()
	defer s.Unlock()

	clean := s.subs[:0]
	for _, o := range s.subs {
		if o != sub {
			clean = append(clean, o)
		}
	}
	s.subs = clean
	return len(s.subs) == 0
}


================================================
FILE: view_test.go
================================================
// Copyright (c) Roman Atachiants and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

package tile

import (
	"testing"
	"unsafe"

	"github.com/stretchr/testify/assert"
)

/*
cpu: 13th Gen Intel(R) Core(TM) i7-13700K
BenchmarkView/write-24         	 9540012	       125.0 ns/op	      48 B/op	       1 allocs/op
BenchmarkView/move-24          	   16141	     74408 ns/op	       0 B/op	       0 allocs/op
*/
func BenchmarkView(b *testing.B) {
	m := mapFrom("300x300.png")
	v := NewView(m, "view 1")
	v.Resize(NewRect(100, 0, 200, 100), nil)

	go func() {
		for range v.Inbox {
		}
	}()

	b.Run("write", func(b *testing.B) {
		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			v.WriteAt(152, 52, Value(0))
		}
	})

	b.Run("move", func(b *testing.B) {
		locs := []Point{
			At(100, 0),
			At(200, 100),
		}

		b.ReportAllocs()
		b.ResetTimer()
		for n := 0; n < b.N; n++ {
			v.MoveAt(locs[n%2], nil)
		}
	})
}

func TestView(t *testing.T) {
	m := mapFrom("300x300.png")

	// Create a new view
	c := counter(0)
	v := NewView(m, "view 1")
	v.Resize(NewRect(100, 0, 200, 100), c.count)
	assert.NotNil(t, v)
	assert.Equal(t, 10000, int(c))

	// Resize to 10x10
	c = counter(0)
	v.Resize(NewRect(0, 0, 10, 10), c.count)
	assert.Equal(t, 100, int(c))

	// Move down-right
	c = counter(0)
	v.MoveBy(2, 2, c.count)
	assert.Equal(t, 48, int(c))

	// Move at location
	c = counter(0)
	v.MoveAt(At(4, 4), c.count)
	assert.Equal(t, 48, int(c))

	// Each
	c = counter(0)
	v.Each(c.count)
	assert.Equal(t, 100, int(c))

	// Update a tile in view
	cursor, _ := v.At(5, 5)
	before := cursor.Value()
	v.WriteAt(5, 5, Value(55))
	update := <-v.Inbox
	assert.Equal(t, At(5, 5), update.New.Point)
	assert.NotEqual(t, before, update.New)

	// Merge a tile in view, but with zero mask (won't do anything)
	cursor, _ = v.At(5, 5)
	before = cursor.Value()
	v.MergeAt(5, 5, Value(66), Value(0)) // zero mask
	update = <-v.Inbox
	assert.Equal(t, At(5, 5), update.New.Point)
	assert.Equal(t, before, update.New.Value)

	// Close the view
	assert.NoError(t, v.Close())
	v.WriteAt(5, 5, Value(66))
	assert.Equal(t, 0, len(v.Inbox))
}

func TestUpdates_Simple(t *testing.T) {
	m := mapFrom("300x300.png")
	c := counter(0)
	v := NewView(m, "view 1")
	v.Resize(NewRect(0, 0, 10, 10), c.count)

	assert.NotNil(t, v)
	assert.Equal(t, 100, int(c))

	// Update a tile in view
	cursor, _ := v.At(5, 5)
	cursor.Write(Value(0xF0))
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(5, 5),
		},
		New: ValueAt{
			Point: At(5, 5),
			Value: Value(0xF0),
		},
	}, <-v.Inbox)

	// Add an object to an observed tile
	cursor.Add("A")
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(5, 5),
			Value: Value(0xF0),
		},
		New: ValueAt{
			Point: At(5, 5),
			Value: Value(0xF0),
		},
		Add: "A",
	}, <-v.Inbox)

	// Delete an object from an observed tile
	cursor.Del("A")
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(5, 5),
			Value: Value(0xF0),
		},
		New: ValueAt{
			Point: At(5, 5),
			Value: Value(0xF0),
		},
		Del: "A",
	}, <-v.Inbox)

	// Mask a tile in view
	cursor.Mask(0xFF, 0x0F)
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(5, 5),
			Value: Value(0xF0),
		},
		New: ValueAt{
			Point: At(5, 5),
			Value: Value(0xFF),
		},
	}, <-v.Inbox)

	// Merge a tile in view
	cursor.Merge(func(v Value) Value {
		return 0xAA
	})
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(5, 5),
			Value: Value(0xFF),
		},
		New: ValueAt{
			Point: At(5, 5),
			Value: Value(0xAA),
		},
	}, <-v.Inbox)
}

func TestMove_Within(t *testing.T) {
	m := mapFrom("300x300.png")
	c := counter(0)
	v := NewView(m, "view 1")
	v.Resize(NewRect(0, 0, 10, 10), c.count)

	// Add an object to an observed tile. This should only fire once since
	// both the old and new states are the observed by the view.
	cursor, _ := v.At(5, 5)
	cursor.Move("A", At(6, 6))
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(5, 5),
		},
		New: ValueAt{
			Point: At(6, 6),
		},
		Del: "A",
		Add: "A",
	}, <-v.Inbox)
}

func TestMove_Incoming(t *testing.T) {
	m := mapFrom("300x300.png")
	c := counter(0)
	v := NewView(m, "view 1")
	v.Resize(NewRect(0, 0, 10, 10), c.count)

	// Add an object to an observed tile from outside the view.
	cursor, _ := v.At(20, 20)
	cursor.Move("A", At(5, 5))
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(20, 20),
		},
		New: ValueAt{
			Point: At(5, 5),
		},
		Del: "A",
		Add: "A",
	}, <-v.Inbox)
}

func TestMove_Outgoing(t *testing.T) {
	m := mapFrom("300x300.png")
	c := counter(0)
	v := NewView(m, "view 1")
	v.Resize(NewRect(0, 0, 10, 10), c.count)

	// Move an object from an observed tile outside of the view.
	cursor, _ := v.At(5, 5)
	cursor.Move("A", At(20, 20))
	assert.Equal(t, Update[string]{
		Old: ValueAt{
			Point: At(5, 5),
		},
		New: ValueAt{
			Point: At(20, 20),
		},
		Del: "A",
		Add: "A",
	}, <-v.Inbox)
}

func TestView_MoveTo(t *testing.T) {
	m := mapFrom("300x300.png")

	// Create a new view
	c := counter(0)
	v := NewView(m, "view 1")
	v.Resize(NewRect(10, 10, 12, 12), c.count)

	assert.NotNil(t, v)
	assert.Equal(t, 4, int(c))
	assert.Equal(t, 9, countObservers(m))

	const distance = 10

	assert.Equal(t, 1, countObserversAt(m, 10, 10))
	for i := 0; i < distance; i++ {
		v.MoveTo(East, 1, c.count)
	}

	assert.Equal(t, 0, countObserversAt(m, 10, 10))
	for i := 0; i < distance; i++ {
		v.MoveTo(South, 1, c.count)
	}

	assert.Equal(t, 0, countObserversAt(m, 10, 10))
	for i := 0; i < distance; i++ {
		v.MoveTo(West, 1, c.count)
	}

	assert.Equal(t, 0, countObserversAt(m, 10, 10))
	for i := 0; i < distance; i++ {
		v.MoveTo(North, 1, c.count)
	}

	// Start should have the observer attached
	assert.Equal(t, 1, countObserversAt(m, 10, 10))
	assert.Equal(t, 0, countObserversAt(m, 100, 100))

	// Count the number of observers, should be the same as before
	assert.Equal(t, 9, countObservers(m))
	assert.NoError(t, v.Close())
}

func TestView_Updates(t *testing.T) {
	m := mapFrom("300x300.png")
	v := NewView(m, "view 1")
	v.Resize(NewRect(10, 10, 15, 15), nil)

	move := func(x1, y1, x2, y2 int16) {
		at, _ := m.At(x1, y1)
		at.Move("A", At(x2, y2))

		assert.Equal(t, Update[string]{
			Old: ValueAt{Point: At(x1, y1)},
			New: ValueAt{Point: At(x2, y2)},
			Del: "A", Add: "A",
		}, <-v.Inbox)
	}

	move(9, 12, 10, 12)  // Enter from left edge
	move(10, 12, 9, 12)  // Exit to left edge
	move(15, 12, 14, 12) // Enter from right edge
	move(14, 12, 15, 12) // Exit to right edge
	move(12, 9, 12, 10)  // Enter from top edge
	move(12, 10, 12, 9)  // Exit to top edge
	move(12, 15, 12, 14) // Enter from bottom edge
	move(12, 14, 12, 15) // Exit to bottom edge
	move(9, 9, 10, 10)   // Enter from top-left diagonal
	move(10, 10, 9, 9)   // Exit to top-left diagonal
	move(15, 9, 14, 10)  // Enter from top-right diagonal
	move(14, 10, 15, 9)  // Exit to top-right diagonal
	move(9, 15, 10, 14)  // Enter from bottom-left diagonal
	move(10, 14, 9, 15)  // Exit to bottom-left diagonal
	move(15, 15, 14, 14) // Enter from bottom-right diagonal
	move(14, 14, 15, 15) // Exit to bottom-right diagonal

	assert.NoError(t, v.Close())
}

func TestSizeUpdate(t *testing.T) {
	assert.Equal(t, 24, int(unsafe.Sizeof(Update[uint32]{})))
}

// ---------------------------------- Mocks ----------------------------------

func countObserversAt(m *Grid[string], x, y int16) (count int) {
	start, _ := m.At(x, y)
	start.Observers(func(view Observer[string]) {
		count++
	})
	return count
}

func countObservers(m *Grid[string]) int {
	var observers int
	m.Each(func(p Point, t Tile[string]) {
		if t.IsObserved() {
			observers++
		}
	})
	return observers
}

type fakeView[T comparable] func(*Update[T])

func (f fakeView[T]) Viewport() Rect {
	return Rect{}
}

func (f fakeView[T]) Resize(r Rect, fn func(Point, Tile[T])) {
	// Do nothing
}

func (f fakeView[T]) onUpdate(e *Update[T]) {
	f(e)
}

type counter int

func (c *counter) count(p Point, tile Tile[string]) {
	*c++
}
Download .txt
gitextract_bvjbbodm/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── grid.go
├── grid_test.go
├── path.go
├── path_test.go
├── point.go
├── point_test.go
├── store.go
├── store_test.go
├── view.go
└── view_test.go
Download .txt
SYMBOL INDEX (191 symbols across 10 files)

FILE: grid.go
  type Grid (line 12) | type Grid struct
  function NewGrid (line 22) | func NewGrid(width, height int16) *Grid[string] {
  function NewGridOf (line 28) | func NewGridOf[T comparable](width, height int16) *Grid[T] {
  method Each (line 57) | func (m *Grid[T]) Each(fn func(Point, Tile[T])) {
  method Within (line 66) | func (m *Grid[T]) Within(nw, se Point, fn func(Point, Tile[T])) {
  method pagesWithin (line 78) | func (m *Grid[T]) pagesWithin(nw, se Point, fn func(*page[T])) {
  method At (line 91) | func (m *Grid[T]) At(x, y int16) (Tile[T], bool) {
  method WriteAt (line 100) | func (m *Grid[T]) WriteAt(x, y int16, tile Value) {
  method MaskAt (line 108) | func (m *Grid[T]) MaskAt(x, y int16, tile, mask Value) {
  method MergeAt (line 115) | func (m *Grid[T]) MergeAt(x, y int16, merge func(Value) Value) {
  method Neighbors (line 122) | func (m *Grid[T]) Neighbors(x, y int16, fn func(Point, Tile[T])) {
  method pageAt (line 154) | func (m *Grid[T]) pageAt(x, y int16) *page[T] {
  type page (line 174) | type page struct
  method tileAt (line 183) | func (p *page[T]) tileAt(idx uint8) Value {
  method IsObserved (line 188) | func (p *page[T]) IsObserved() bool {
  method Bounds (line 193) | func (p *page[T]) Bounds() Rect {
  method At (line 198) | func (p *page[T]) At(grid *Grid[T], x, y int16) Tile[T] {
  method Each (line 203) | func (p *page[T]) Each(grid *Grid[T], fn func(Point, Tile[T])) {
  method SetObserved (line 217) | func (p *page[T]) SetObserved(observed bool) {
  method Lock (line 236) | func (p *page[T]) Lock() {
  method Unlock (line 242) | func (p *page[T]) Unlock() {
  method writeTile (line 249) | func (p *page[T]) writeTile(grid *Grid[T], idx uint8, after Value) {
  method mergeTile (line 272) | func (p *page[T]) mergeTile(grid *Grid[T], idx uint8, fn func(Value) Val...
  method addObject (line 302) | func (p *page[T]) addObject(idx uint8, object T) (value uint32) {
  method delObject (line 318) | func (p *page[T]) delObject(idx uint8, object T) (value uint32) {
  type Tile (line 331) | type Tile struct
  method Count (line 338) | func (t Tile[T]) Count() (count int) {
  method Point (line 350) | func (t Tile[T]) Point() Point {
  method Value (line 355) | func (t Tile[T]) Value() Value {
  method Range (line 360) | func (t Tile[T]) Range(fn func(T) error) error {
  method IsObserved (line 374) | func (t Tile[T]) IsObserved() bool {
  method Observers (line 379) | func (t Tile[T]) Observers(fn func(view Observer[T])) {
  method Add (line 392) | func (t Tile[T]) Add(v T) {
  method Del (line 413) | func (t Tile[T]) Del(v T) {
  method Move (line 434) | func (t Tile[T]) Move(v T, dst Point) bool {
  method Write (line 476) | func (t Tile[T]) Write(tile Value) {
  method Merge (line 481) | func (t Tile[T]) Merge(merge func(Value) Value) Value {
  method Mask (line 487) | func (t Tile[T]) Mask(tile, mask Value) Value {
  function pointOf (line 494) | func pointOf(page Point, idx uint8) Point {

FILE: grid_test.go
  function BenchmarkGrid (line 26) | func BenchmarkGrid(b *testing.B) {
  function BenchmarkState (line 108) | func BenchmarkState(b *testing.B) {
  function TestPageSize (line 146) | func TestPageSize(t *testing.T) {
  function TestWithin (line 152) | func TestWithin(t *testing.T) {
  function TestWithinCorner (line 168) | func TestWithinCorner(t *testing.T) {
  function TestWithinXY (line 182) | func TestWithinXY(t *testing.T) {
  function TestWithinOneSide (line 186) | func TestWithinOneSide(t *testing.T) {
  function TestWithinInvalid (line 201) | func TestWithinInvalid(t *testing.T) {
  function TestEach (line 210) | func TestEach(t *testing.T) {
  function TestNeighbors (line 231) | func TestNeighbors(t *testing.T) {
  function TestAt (line 260) | func TestAt(t *testing.T) {
  function TestUpdate (line 283) | func TestUpdate(t *testing.T) {
  function TestState (line 309) | func TestState(t *testing.T) {
  function TestStateRangeErr (line 328) | func TestStateRangeErr(t *testing.T) {
  function TestPointOf (line 341) | func TestPointOf(t *testing.T) {
  function TestConcurrentMerge (line 375) | func TestConcurrentMerge(t *testing.T) {

FILE: path.go
  type edge (line 17) | type edge struct
  method Around (line 23) | func (m *Grid[T]) Around(from Point, distance uint32, costOf costFn, fn ...
  method Path (line 69) | func (m *Grid[T]) Path(from, to Point, costOf costFn) ([]Point, int, boo...
  function encode (line 141) | func encode(cost uint32, dir Direction) uint32 {
  function decode (line 146) | func decode(value uint32) (cost uint32, dir Direction) {
  type pathfinder (line 154) | type pathfinder struct
  function acquire (line 169) | func acquire(capacity int) *pathfinder {
  function release (line 179) | func release(v *pathfinder) {
  type frontier (line 189) | type frontier struct
    method Reset (line 203) | func (q *frontier) Reset() {
    method IsEmpty (line 226) | func (q *frontier) IsEmpty() bool {
    method Push (line 230) | func (q *frontier) Push(value, priority uint32) {
    method Pop (line 239) | func (q *frontier) Pop() uint32 {
  function newFrontier (line 195) | func newFrontier() *frontier {

FILE: path_test.go
  function TestPath (line 18) | func TestPath(t *testing.T) {
  function TestPathTiny (line 37) | func TestPathTiny(t *testing.T) {
  function TestDraw (line 51) | func TestDraw(t *testing.T) {
  function BenchmarkPath (line 66) | func BenchmarkPath(b *testing.B) {
  function BenchmarkAround (line 137) | func BenchmarkAround(b *testing.B) {
  function TestAround (line 164) | func TestAround(t *testing.T) {
  function TestAroundMiss (line 180) | func TestAroundMiss(t *testing.T) {
  function BenchmarkHeap (line 191) | func BenchmarkHeap(b *testing.B) {
  function rand (line 205) | func rand(i int) uint32 {
  function costOf (line 215) | func costOf(tile Value) uint16 {
  function mapFrom (line 223) | func mapFrom(name string) *Grid[string] {
  function plotPath (line 253) | func plotPath(m *Grid[string], path []Point) string {
  function pointInPath (line 280) | func pointInPath(point Point, path []Point) bool {
  function drawGrid (line 290) | func drawGrid(m *Grid[string], rect Rect) image.Image {

FILE: point.go
  type Point (line 14) | type Point struct
    method String (line 29) | func (p Point) String() string {
    method Integer (line 34) | func (p Point) Integer() uint32 {
    method Equal (line 39) | func (p Point) Equal(other Point) bool {
    method Add (line 44) | func (p Point) Add(p2 Point) Point {
    method Subtract (line 49) | func (p Point) Subtract(p2 Point) Point {
    method Multiply (line 54) | func (p Point) Multiply(p2 Point) Point {
    method Divide (line 59) | func (p Point) Divide(p2 Point) Point {
    method MultiplyScalar (line 64) | func (p Point) MultiplyScalar(s int16) Point {
    method DivideScalar (line 69) | func (p Point) DivideScalar(s int16) Point {
    method Within (line 74) | func (p Point) Within(nw, se Point) bool {
    method WithinRect (line 79) | func (p Point) WithinRect(box Rect) bool {
    method WithinSize (line 85) | func (p Point) WithinSize(size Point) bool {
    method Move (line 90) | func (p Point) Move(direction Direction) Point {
    method MoveBy (line 95) | func (p Point) MoveBy(direction Direction, n int16) Point {
    method DistanceTo (line 119) | func (p Point) DistanceTo(other Point) uint32 {
    method Angle (line 124) | func (p Point) Angle(other Point) Direction {
  function unpackPoint (line 19) | func unpackPoint(v uint32) Point {
  function At (line 24) | func At(x, y int16) Point {
  function abs (line 139) | func abs(n int32) uint32 {
  type Rect (line 149) | type Rect struct
    method Contains (line 161) | func (a Rect) Contains(p Point) bool {
    method Intersects (line 166) | func (a Rect) Intersects(b Rect) bool {
    method Size (line 171) | func (a *Rect) Size() Point {
    method IsZero (line 179) | func (a Rect) IsZero() bool {
    method Difference (line 185) | func (a Rect) Difference(b Rect) (result [4]Rect) {
    method pack (line 230) | func (a Rect) pack() uint64 {
  function NewRect (line 156) | func NewRect(left, top, right, bottom int16) Rect {
  function unpackRect (line 235) | func unpackRect(v uint64) Rect {
  type Direction (line 245) | type Direction
    method String (line 260) | func (v Direction) String() string {
    method Vector (line 284) | func (v Direction) Vector(scale int16) Point {
  constant North (line 249) | North Direction = iota
  constant NorthEast (line 250) | NorthEast
  constant East (line 251) | East
  constant SouthEast (line 252) | SouthEast
  constant South (line 253) | South
  constant SouthWest (line 254) | SouthWest
  constant West (line 255) | West
  constant NorthWest (line 256) | NorthWest
  function angleOf (line 289) | func angleOf(from, to Point) Direction {
  function oppositeDirection (line 316) | func oppositeDirection(dir Direction) Direction {

FILE: point_test.go
  function BenchmarkPoint (line 17) | func BenchmarkPoint(b *testing.B) {
  function TestPoint (line 36) | func TestPoint(t *testing.T) {
  function TestIntersects (line 58) | func TestIntersects(t *testing.T) {
  function TestDirection (line 64) | func TestDirection(t *testing.T) {
  function TestDirection_Empty (line 71) | func TestDirection_Empty(t *testing.T) {
  function TestPointAngle (line 76) | func TestPointAngle(t *testing.T) {
  function TestMove (line 137) | func TestMove(t *testing.T) {
  function TestContains (line 158) | func TestContains(t *testing.T) {
  function TestDiff_Right (line 176) | func TestDiff_Right(t *testing.T) {
  function TestDiff_Left (line 185) | func TestDiff_Left(t *testing.T) {
  function TestDiff_Up (line 194) | func TestDiff_Up(t *testing.T) {
  function TestDiff_Down (line 203) | func TestDiff_Down(t *testing.T) {

FILE: store.go
  constant tileDataSize (line 16) | tileDataSize = int(unsafe.Sizeof([9]Value{}))
  method WriteTo (line 21) | func (m *Grid[T]) WriteTo(dst io.Writer) (n int64, err error) {
  function ReadFrom (line 47) | func ReadFrom[T comparable](src io.Reader) (grid *Grid[T], err error) {
  method WriteFile (line 77) | func (m *Grid[T]) WriteFile(filename string) error {
  function ReadFile (line 97) | func ReadFile[T comparable](filename string) (grid *Grid[T], err error) {

FILE: store_test.go
  function BenchmarkStore (line 20) | func BenchmarkStore(b *testing.B) {
  function TestSaveLoad (line 47) | func TestSaveLoad(t *testing.T) {
  function TestSaveLoadFlate (line 62) | func TestSaveLoadFlate(t *testing.T) {
  function TestSaveLoadFile (line 83) | func TestSaveLoadFile(t *testing.T) {

FILE: view.go
  type Observer (line 12) | type Observer interface
  type ValueAt (line 19) | type ValueAt struct
  type Update (line 25) | type Update struct
  type View (line 36) | type View struct
  function NewView (line 45) | func NewView[S any, T comparable](m *Grid[T], state S) *View[S, T] {
  method Viewport (line 56) | func (v *View[S, T]) Viewport() Rect {
  method Resize (line 61) | func (v *View[S, T]) Resize(view Rect, fn func(Point, Tile[T])) {
  method MoveTo (line 100) | func (v *View[S, T]) MoveTo(angle Direction, distance int16, fn func(Poi...
  method MoveBy (line 110) | func (v *View[S, T]) MoveBy(x, y int16, fn func(Point, Tile[T])) {
  method MoveAt (line 119) | func (v *View[S, T]) MoveAt(nw Point, fn func(Point, Tile[T])) {
  method Each (line 129) | func (v *View[S, T]) Each(fn func(Point, Tile[T])) {
  method At (line 135) | func (v *View[S, T]) At(x, y int16) (Tile[T], bool) {
  method WriteAt (line 140) | func (v *View[S, T]) WriteAt(x, y int16, tile Value) {
  method MergeAt (line 146) | func (v *View[S, T]) MergeAt(x, y int16, tile, mask Value) {
  method Close (line 151) | func (v *View[S, T]) Close() error {
  method onUpdate (line 162) | func (v *View[S, T]) onUpdate(ev *Update[T]) {
  type pubsub (line 169) | type pubsub struct
  method Subscribe (line 175) | func (p *pubsub[T]) Subscribe(page Point, sub Observer[T]) bool {
  method Unsubscribe (line 186) | func (p *pubsub[T]) Unsubscribe(page Point, sub Observer[T]) bool {
  method Notify1 (line 194) | func (p *pubsub[T]) Notify1(ev *Update[T], page Point) {
  method Notify2 (line 204) | func (p *pubsub[T]) Notify2(ev *Update[T], pages [2]Point) {
  method Each1 (line 214) | func (p *pubsub[T]) Each1(fn func(sub Observer[T]), page Point) {
  method Each2 (line 223) | func (p *pubsub[T]) Each2(fn func(sub Observer[T]), pages [2]Point) {
  type observers (line 247) | type observers struct
  function newObservers (line 253) | func newObservers[T comparable]() *observers[T] {
  method Each (line 260) | func (s *observers[T]) Each(fn func(sub Observer[T])) {
  method Subscribe (line 273) | func (s *observers[T]) Subscribe(sub Observer[T]) bool {
  method Unsubscribe (line 281) | func (s *observers[T]) Unsubscribe(sub Observer[T]) bool {

FILE: view_test.go
  function BenchmarkView (line 18) | func BenchmarkView(b *testing.B) {
  function TestView (line 50) | func TestView(t *testing.T) {
  function TestUpdates_Simple (line 102) | func TestUpdates_Simple(t *testing.T) {
  function TestMove_Within (line 181) | func TestMove_Within(t *testing.T) {
  function TestMove_Incoming (line 203) | func TestMove_Incoming(t *testing.T) {
  function TestMove_Outgoing (line 224) | func TestMove_Outgoing(t *testing.T) {
  function TestView_MoveTo (line 245) | func TestView_MoveTo(t *testing.T) {
  function TestView_Updates (line 288) | func TestView_Updates(t *testing.T) {
  function TestSizeUpdate (line 324) | func TestSizeUpdate(t *testing.T) {
  function countObserversAt (line 330) | func countObserversAt(m *Grid[string], x, y int16) (count int) {
  function countObservers (line 338) | func countObservers(m *Grid[string]) int {
  type fakeView (line 348) | type fakeView
  method Viewport (line 350) | func (f fakeView[T]) Viewport() Rect {
  method Resize (line 354) | func (f fakeView[T]) Resize(r Rect, fn func(Point, Tile[T])) {
  method onUpdate (line 358) | func (f fakeView[T]) onUpdate(e *Update[T]) {
  type counter (line 362) | type counter
    method count (line 364) | func (c *counter) count(p Point, tile Tile[string]) {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (99K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 19,
    "preview": "github: [kelindar]\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 700,
    "preview": "name: Test\r\non: [push, pull_request]\r\nenv:\r\n  GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }}\r\n  GO111MODULE: \"on\"\r\njobs:\r\n"
  },
  {
    "path": ".gitignore",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2020 Roman Atachiants \n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 15303,
    "preview": "# Tile: Data-Oriented 2D Grid Engine\n\n<p align=\"center\">\n    <img width=\"200\" height=\"100\" src=\"./.github/logo.png\">\n   "
  },
  {
    "path": "go.mod",
    "chars": 310,
    "preview": "module github.com/kelindar/tile\n\ngo 1.25\n\nrequire (\n\tgithub.com/kelindar/intmap v1.5.0\n\tgithub.com/kelindar/iostream v1."
  },
  {
    "path": "go.sum",
    "chars": 1229,
    "preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.m"
  },
  {
    "path": "grid.go",
    "chars": 12943,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE "
  },
  {
    "path": "grid_test.go",
    "chars": 9665,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE"
  },
  {
    "path": "path.go",
    "chars": 6488,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE "
  },
  {
    "path": "path_test.go",
    "chars": 6931,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE "
  },
  {
    "path": "point.go",
    "chars": 7884,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE "
  },
  {
    "path": "point_test.go",
    "chars": 6131,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE "
  },
  {
    "path": "store.go",
    "chars": 3062,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE"
  },
  {
    "path": "store_test.go",
    "chars": 2338,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE"
  },
  {
    "path": "view.go",
    "chars": 7877,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE "
  },
  {
    "path": "view_test.go",
    "chars": 8399,
    "preview": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE"
  }
]

About this extraction

This page contains the full source code of the kelindar/tile GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (88.2 KB), approximately 28.5k tokens, and a symbol index with 191 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!