[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [kelindar]\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\r\non: [push, pull_request]\r\nenv:\r\n  GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }}\r\n  GO111MODULE: \"on\"\r\njobs:\r\n  test:\r\n    name: Test with Coverage\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - name: Set up Go\r\n        uses: actions/setup-go@v1\r\n        with:\r\n          go-version: 1.23\r\n      - name: Check out code\r\n        uses: actions/checkout@v2\r\n      - name: Install dependencies\r\n        run: |\r\n          go mod download\r\n      - name: Run Unit Tests\r\n        run: |\r\n          go test -race -covermode atomic -coverprofile=profile.cov ./...\r\n      - name: Upload Coverage\r\n        uses: shogo82148/actions-goveralls@v1\r\n        with:\r\n          path-to-profile: profile.cov\r\n"
  },
  {
    "path": ".gitignore",
    "content": ""
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Roman Atachiants \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Tile: Data-Oriented 2D Grid Engine\n\n<p align=\"center\">\n    <img width=\"200\" height=\"100\" src=\"./.github/logo.png\">\n    <br>\n    <img src=\"https://img.shields.io/github/go-mod/go-version/kelindar/tile\" alt=\"Go Version\">\n    <a href=\"https://pkg.go.dev/github.com/kelindar/tile\"><img src=\"https://pkg.go.dev/badge/github.com/kelindar/tile\" alt=\"PkgGoDev\"></a>\n    <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>\n    <a href=\"https://opensource.org/licenses/MIT\"><img src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License\"></a>\n    <a href=\"https://coveralls.io/github/kelindar/tile\"><img src=\"https://coveralls.io/repos/github/kelindar/tile/badge.svg\" alt=\"Coverage\"></a>\n</p>\n\nThis 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.\n\n- **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.\n- **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.\n- **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.\n- **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.\n- **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.\n\n_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!\n\n# Grid & Tiles\n\nThe 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\n\n- Value `uint32` of the tile, that can be used for calculating navigation or quickly retrieve sprite index.\n- 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.\n\nGranted, 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.\n\nIn 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.\n\n```go\ngrid := tile.NewGridOf[string](1000, 1000)\n```\n\nThe `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.\n\n```go\ngrid.Each(func(p Point, t tile.Tile[string]) {\n    // ...\n})\n```\n\nThe `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.\n\n```go\ngrid.Within(At(1, 1), At(5, 5), func(p Point, t tile.Tile[string]) {\n    // ...\n})\n```\n\nThe `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.\n\n```go\nif tile, ok := grid.At(50, 100); ok {\n    // ...\n}\n```\n\nThe `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).\n\n```go\ngrid.WriteAt(50, 100, tile.Value(0xFF))\n```\n\nThe `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.\n\n```go\ngrid.Neighbors(50, 100, func(point tile.Point, t tile.Tile[string]) {\n    // ...\n})\n```\n\nThe `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.\n\n```go\ngrid.MergeAt(50, 100, func(v Value) Value {\n    v += 1\n    return v\n})\n```\n\nThe `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()`.\n\n```go\n// assume byte[0] of the tile is 0b01010001\ngrid.MaskAt(50, 100,\n    0b00101110, // Only last 2 bits matter\n    0b00000011 // Mask specifies that we want to update last 2 bits\n)\n\n// If the original is currently: 0b01010001\n// ...the result result will be: 0b01010010\n```\n\n# Pathfinding\n\nAs 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.\n\nThe `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.\n\n```go\nfrom := At(1, 1)\ngoal := At(7, 7)\npath, distance, found := m.Path(from, goal, func(v tile.Value) uint16{\n    if isImpassable(v) {\n        return 0\n    }\n    return 1\n})\n```\n\nThe `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.\n\n```go\npoint  := At(50, 50)\nradius := 5\nm.Around(point, radius, func(v tile.Value) uint16{\n    if isImpassable(v) {\n        return 0\n    }\n    return 1\n}, func(p tile.Point, t tile.Tile[string]) {\n    // ... tile found\n})\n```\n\n# Observers\n\nGiven 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.\n\nIn 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.\n\nIn the example below we create a new 20x20 view on the grid and iterate through all of the tiles in the view.\n\n```go\nview := tile.NewView[string, string](grid, \"My View #1\")\nview.Resize(tile.NewRect(0, 0, 20, 20), func(p tile.Point, t tile.Tile){\n    // Optional, all of the tiles that are in the view now\n})\n\n// Poll the inbox (in reality this would need to be with a select, and a goroutine)\nfor {\n    update := <-view.Inbox\n    // Do something with update.Point, update.Tile\n}\n```\n\nThe `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).\n\n```go\nview.MoveBy(0, 5, func(p tile.Point, tile tile.Tile){\n    // Every tile which entered our view\n})\n```\n\nSimilarly, `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.\n\n```go\nview.MoveAt(At(10, 10), func(p tile.Point, t tile.Tile){\n    // Every tile which entered our view\n})\n```\n\nThe `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.\n\n```go\nviewRect := tile.NewRect(10, 10, 30, 30)\nview.Resize(viewRect, func(p tile.Point, t tile.Tile){\n    // Every tile which entered our view\n})\n```\n\nThe `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.\n\n```go\n// Unsubscribe from notifications and close the view\nview.Close()\n```\n\n# Save & Load\n\nThe 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.\n\nThe `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.\n\n```go\n// Prepare the output buffer and compressor\noutput := new(bytes.Buffer)\nwriter, err := flate.NewWriter(output, flate.BestSpeed)\nif err != nil {\n    // ...\n}\n\ndefer writer.Close()            // Make sure we flush the compressor\n_, err := grid.WriteTo(writer)  // Write the grid\nif err != nil {\n    // ...\n}\n```\n\nThe `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.\n\n```go\n// Prepare a compressed reader over the buffer\nreader := flate.NewReader(output)\n\n// Read the Grid\ngrid, err := ReadFrom(reader)\nif err != nil{\n    // ...\n}\n```\n\n# Benchmarks\n\nThis 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.\n\n```\ncpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz\nBenchmarkGrid/each-8                 868    1358434 ns/op           0 B/op   0 allocs/op\nBenchmarkGrid/neighbors-8       66551679      17.87 ns/op           0 B/op   0 allocs/op\nBenchmarkGrid/within-8             27207      44753 ns/op           0 B/op   0 allocs/op\nBenchmarkGrid/at-8             399067512      2.994 ns/op           0 B/op   0 allocs/op\nBenchmarkGrid/write-8          130207965      9.294 ns/op           0 B/op   0 allocs/op\nBenchmarkGrid/merge-8          124156794      9.663 ns/op           0 B/op   0 allocs/op\nBenchmarkGrid/mask-8           100000000      10.67 ns/op           0 B/op   0 allocs/op\nBenchmarkState/range-8          12106854      98.91 ns/op           0 B/op   0 allocs/op\nBenchmarkState/add-8            48827727      25.43 ns/op           0 B/op   0 allocs/op\nBenchmarkState/del-8            52110474      21.59 ns/op           0 B/op   0 allocs/op\nBenchmarkPath/9x9-8               264586        4656 ns/op      16460 B/op   3 allocs/op\nBenchmarkPath/300x300-8              601     1937662 ns/op    7801502 B/op   4 allocs/op\nBenchmarkPath/381x381-8              363     3304134 ns/op   62394356 B/op   5 allocs/op\nBenchmarkPath/384x384-8              171     7165777 ns/op   62394400 B/op   5 allocs/op\nBenchmarkPath/3069x3069-8             31    36479106 ns/op  124836075 B/op   4 allocs/op\nBenchmarkPath/3072x3072-8             30    34889740 ns/op  124837686 B/op   4 allocs/op\nBenchmarkPath/6144x6144-8            142     7594013 ns/op   62395376 B/op   3 allocs/op\nBenchmarkAround/3r-8              506857        2384 ns/op        385 B/op   1 allocs/op\nBenchmarkAround/5r-8              214280        5539 ns/op        922 B/op   2 allocs/op\nBenchmarkAround/10r-8              85723       14017 ns/op       3481 B/op   2 allocs/op\nBenchmarkPoint/within-8       1000000000      0.2190 ns/op          0 B/op   0 allocs/op\nBenchmarkPoint/within-rect-8  1000000000      0.2195 ns/op          0 B/op   0 allocs/op\nBenchmarkStore/save-8              14577       82510 ns/op          8 B/op   1 allocs/op\nBenchmarkStore/read-8               3199      364771 ns/op     647419 B/op   7 allocs/op\nBenchmarkView/write-8            6285351       188.2 ns/op         48 B/op   1 allocs/op\nBenchmarkView/move-8               10000      116953 ns/op          0 B/op   0 allocs/op\n```\n\n# Contributing\n\nWe 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/)\n\n## License\n\nTile is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "go.mod",
    "content": "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.4.0\n\tgithub.com/stretchr/testify v1.10.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/kelindar/intmap v1.5.0 h1:VY+AdO4Wx1sF1vGiTkS8n2lxhmFgOQwCIFuePQP4Iqw=\ngithub.com/kelindar/intmap v1.5.0/go.mod h1:NkypxhfaklmDTJqwano3Q1BWk6je77qgQwszDwu8Kc8=\ngithub.com/kelindar/iostream v1.4.0 h1:ELKlinnM/K3GbRp9pYhWuZOyBxMMlYAfsOP+gauvZaY=\ngithub.com/kelindar/iostream v1.4.0/go.mod h1:MkjMuVb6zGdPQVdwLnFRO0xOTOdDvBWTztFmjRDQkXk=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "grid.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for details.\n\npackage tile\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\n// Grid represents a 2D tile map. Internally, a map is composed of 3x3 pages.\ntype Grid[T comparable] struct {\n\tpages      []page[T] // The pages of the map\n\tpageWidth  int16     // The max page width\n\tpageHeight int16     // The max page height\n\tobservers  pubsub[T] // The map of observers\n\tSize       Point     // The map size\n}\n\n// NewGrid returns a new map of the specified size. The width and height must be both\n// multiples of 3.\nfunc NewGrid(width, height int16) *Grid[string] {\n\treturn NewGridOf[string](width, height)\n}\n\n// NewGridOf returns a new map of the specified size. The width and height must be both\n// multiples of 3.\nfunc NewGridOf[T comparable](width, height int16) *Grid[T] {\n\twidth, height = width/3, height/3\n\n\tmax := int32(width) * int32(height)\n\tpages := make([]page[T], max)\n\tm := &Grid[T]{\n\t\tpages:      pages,\n\t\tpageWidth:  width,\n\t\tpageHeight: height,\n\t\tSize:       At(width*3, height*3),\n\t\tobservers: pubsub[T]{\n\t\t\ttmp: sync.Pool{\n\t\t\t\tNew: func() any { return make(map[Observer[T]]struct{}, 4) },\n\t\t\t},\n\t\t},\n\t}\n\n\t// Function to calculate a point based on the index\n\tvar pointAt func(i int) Point = func(i int) Point {\n\t\treturn At(int16(i%int(width)), int16(i/int(width)))\n\t}\n\n\tfor i := 0; i < int(max); i++ {\n\t\tpages[i].point = pointAt(i).MultiplyScalar(3)\n\t}\n\treturn m\n}\n\n// Each iterates over all of the tiles in the map.\nfunc (m *Grid[T]) Each(fn func(Point, Tile[T])) {\n\tuntil := int(m.pageHeight) * int(m.pageWidth)\n\tfor i := 0; i < until; i++ {\n\t\tm.pages[i].Each(m, fn)\n\t}\n}\n\n// Within selects the tiles within a specifid bounding box which is specified by\n// north-west and south-east coordinates.\nfunc (m *Grid[T]) Within(nw, se Point, fn func(Point, Tile[T])) {\n\tm.pagesWithin(nw, se, func(page *page[T]) {\n\t\tpage.Each(m, func(p Point, v Tile[T]) {\n\t\t\tif p.Within(nw, se) {\n\t\t\t\tfn(p, v)\n\t\t\t}\n\t\t})\n\t})\n}\n\n// pagesWithin selects the pages within a specifid bounding box which is specified\n// by north-west and south-east coordinates.\nfunc (m *Grid[T]) pagesWithin(nw, se Point, fn func(*page[T])) {\n\tif !se.WithinSize(m.Size) {\n\t\tse = At(m.Size.X-1, m.Size.Y-1)\n\t}\n\n\tfor x := nw.X / 3; x <= se.X/3; x++ {\n\t\tfor y := nw.Y / 3; y <= se.Y/3; y++ {\n\t\t\tfn(m.pageAt(x, y))\n\t\t}\n\t}\n}\n\n// At returns the tile at a specified position\nfunc (m *Grid[T]) At(x, y int16) (Tile[T], bool) {\n\tif x >= 0 && y >= 0 && x < m.Size.X && y < m.Size.Y {\n\t\treturn m.pageAt(x/3, y/3).At(m, x, y), true\n\t}\n\n\treturn Tile[T]{}, false\n}\n\n// WriteAt updates the entire tile value at a specific coordinate\nfunc (m *Grid[T]) WriteAt(x, y int16, tile Value) {\n\tif x >= 0 && y >= 0 && x < m.Size.X && y < m.Size.Y {\n\t\tm.pageAt(x/3, y/3).writeTile(m, uint8((y%3)*3+(x%3)), tile)\n\t}\n}\n\n// MaskAt atomically updates the bits of tile at a specific coordinate. The bits are\n// specified by the mask. The bits that need to be updated should be flipped on in the mask.\nfunc (m *Grid[T]) MaskAt(x, y int16, tile, mask Value) {\n\tm.MergeAt(x, y, func(value Value) Value {\n\t\treturn (value &^ mask) | (tile & mask)\n\t})\n}\n\n// Merge atomically merges the tile by applying a merging function at a specific coordinate.\nfunc (m *Grid[T]) MergeAt(x, y int16, merge func(Value) Value) {\n\tif x >= 0 && y >= 0 && x < m.Size.X && y < m.Size.Y {\n\t\tm.pageAt(x/3, y/3).mergeTile(m, uint8((y%3)*3+(x%3)), merge)\n\t}\n}\n\n// Neighbors iterates over the direct neighbouring tiles\nfunc (m *Grid[T]) Neighbors(x, y int16, fn func(Point, Tile[T])) {\n\n\t// First we need to figure out which pages contain the neighboring tiles and\n\t// then load them. In the best-case we need to load only a single page. In\n\t// the worst-case: we need to load 3 pages.\n\tnX, nY := x/3, (y-1)/3 // North\n\teX, eY := (x+1)/3, y/3 // East\n\tsX, sY := x/3, (y+1)/3 // South\n\twX, wY := (x-1)/3, y/3 // West\n\n\t// Get the North\n\tif y > 0 {\n\t\tfn(At(x, y-1), m.pageAt(nX, nY).At(m, x, y-1))\n\t}\n\n\t// Get the East\n\tif eX < m.pageWidth {\n\t\tfn(At(x+1, y), m.pageAt(eX, eY).At(m, x+1, y))\n\t}\n\n\t// Get the South\n\tif sY < m.pageHeight {\n\t\tfn(At(x, y+1), m.pageAt(sX, sY).At(m, x, y+1))\n\t}\n\n\t// Get the West\n\tif x > 0 {\n\t\tfn(At(x-1, y), m.pageAt(wX, wY).At(m, x-1, y))\n\t}\n}\n\n// pageAt loads a page at a given page location\nfunc (m *Grid[T]) pageAt(x, y int16) *page[T] {\n\tindex := int(x) + int(m.pageWidth)*int(y)\n\n\t// Eliminate bounds checks\n\tif index >= 0 && index < len(m.pages) {\n\t\treturn &m.pages[index]\n\t}\n\n\treturn nil\n}\n\n// ---------------------------------- Tile ----------------------------------\n\n// Value represents a packed tile information, it must fit on 4 bytes.\ntype Value = uint32\n\n// ---------------------------------- Page ----------------------------------\n\n// page represents a 3x3 tile page each page should neatly fit on a cache\n// line and speed things up.\ntype page[T comparable] struct {\n\tmu    sync.Mutex  // State lock, 8 bytes\n\tstate map[T]uint8 // State data, 8 bytes\n\tflags uint32      // Page flags, 4 bytes\n\tpoint Point       // Page X, Y coordinate, 4 bytes\n\ttiles [9]Value    // Page tiles, 36 bytes\n}\n\n// tileAt reads a tile at a page index\nfunc (p *page[T]) tileAt(idx uint8) Value {\n\treturn Value(atomic.LoadUint32((*uint32)(&p.tiles[idx])))\n}\n\n// IsObserved returns whether the tile is observed or not\nfunc (p *page[T]) IsObserved() bool {\n\treturn (atomic.LoadUint32(&p.flags))&1 != 0\n}\n\n// Bounds returns the bounding box for the tile page.\nfunc (p *page[T]) Bounds() Rect {\n\treturn Rect{p.point, At(p.point.X+3, p.point.Y+3)}\n}\n\n// At returns a cursor at a specific coordinate\nfunc (p *page[T]) At(grid *Grid[T], x, y int16) Tile[T] {\n\treturn Tile[T]{grid: grid, data: p, idx: uint8((y%3)*3 + (x % 3))}\n}\n\n// Each iterates over all of the tiles in the page.\nfunc (p *page[T]) Each(grid *Grid[T], fn func(Point, Tile[T])) {\n\tx, y := p.point.X, p.point.Y\n\tfn(Point{x, y}, Tile[T]{grid: grid, data: p, idx: 0})         // NW\n\tfn(Point{x + 1, y}, Tile[T]{grid: grid, data: p, idx: 1})     // N\n\tfn(Point{x + 2, y}, Tile[T]{grid: grid, data: p, idx: 2})     // NE\n\tfn(Point{x, y + 1}, Tile[T]{grid: grid, data: p, idx: 3})     // W\n\tfn(Point{x + 1, y + 1}, Tile[T]{grid: grid, data: p, idx: 4}) // C\n\tfn(Point{x + 2, y + 1}, Tile[T]{grid: grid, data: p, idx: 5}) // E\n\tfn(Point{x, y + 2}, Tile[T]{grid: grid, data: p, idx: 6})     // SW\n\tfn(Point{x + 1, y + 2}, Tile[T]{grid: grid, data: p, idx: 7}) // S\n\tfn(Point{x + 2, y + 2}, Tile[T]{grid: grid, data: p, idx: 8}) // SE\n}\n\n// SetObserved sets the observed flag on the page\nfunc (p *page[T]) SetObserved(observed bool) {\n\tconst flagObserved = 0x1\n\tfor {\n\t\tvalue := atomic.LoadUint32(&p.flags)\n\t\tmerge := value\n\t\tif observed {\n\t\t\tmerge = value | flagObserved\n\t\t} else {\n\t\t\tmerge = value &^ flagObserved\n\t\t}\n\n\t\tif atomic.CompareAndSwapUint32(&p.flags, value, merge) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// Lock locks the state. Note: this needs to be named Lock() so go vet will\n// complain if the page is copied around.\nfunc (p *page[T]) Lock() {\n\tp.mu.Lock()\n}\n\n// Unlock unlocks the state. Note: this needs to be named Unlock() so go vet will\n// complain if the page is copied around.\nfunc (p *page[T]) Unlock() {\n\tp.mu.Unlock()\n}\n\n// ---------------------------------- Mutations ----------------------------------\n\n// writeTile stores the tile and return  whether tile is observed or not\nfunc (p *page[T]) writeTile(grid *Grid[T], idx uint8, after Value) {\n\tbefore := p.tileAt(idx)\n\tfor !atomic.CompareAndSwapUint32(&p.tiles[idx], uint32(before), uint32(after)) {\n\t\tbefore = p.tileAt(idx)\n\t}\n\n\t// If observed, notify the observers of the tile\n\tif p.IsObserved() {\n\t\tat := pointOf(p.point, idx)\n\t\tgrid.observers.Notify1(&Update[T]{\n\t\t\tOld: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: before,\n\t\t\t},\n\t\t\tNew: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: after,\n\t\t\t},\n\t\t}, p.point)\n\t}\n}\n\n// mergeTile atomically merges the tile bits given a function\nfunc (p *page[T]) mergeTile(grid *Grid[T], idx uint8, fn func(Value) Value) Value {\n\tbefore := p.tileAt(idx)\n\tafter := fn(before)\n\n\t// Swap, if we're not able to re-merge again\n\tfor !atomic.CompareAndSwapUint32(&p.tiles[idx], uint32(before), uint32(after)) {\n\t\tbefore = p.tileAt(idx)\n\t\tafter = fn(before)\n\t}\n\n\t// If observed, notify the observers of the tile\n\tif p.IsObserved() {\n\t\tat := pointOf(p.point, idx)\n\t\tgrid.observers.Notify1(&Update[T]{\n\t\t\tOld: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: before,\n\t\t\t},\n\t\t\tNew: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: after,\n\t\t\t},\n\t\t}, p.point)\n\t}\n\n\t// Return the merged tile data\n\treturn after\n}\n\n// addObject adds object to the set\nfunc (p *page[T]) addObject(idx uint8, object T) (value uint32) {\n\tp.Lock()\n\n\t// Lazily initialize the map, as most pages might not have anything stored\n\t// in them (e.g. water or empty tile)\n\tif p.state == nil {\n\t\tp.state = make(map[T]uint8)\n\t}\n\n\tp.state[object] = uint8(idx)\n\tvalue = p.tileAt(idx)\n\tp.Unlock()\n\treturn\n}\n\n// delObject removes the object from the set\nfunc (p *page[T]) delObject(idx uint8, object T) (value uint32) {\n\tp.Lock()\n\tif p.state != nil {\n\t\tdelete(p.state, object)\n\t}\n\tvalue = p.tileAt(idx)\n\tp.Unlock()\n\treturn\n}\n\n// ---------------------------------- Tile Cursor ----------------------------------\n\n// Tile represents an iterator over all state objects at a particular location.\ntype Tile[T comparable] struct {\n\tgrid *Grid[T] // grid pointer\n\tdata *page[T] // page pointer\n\tidx  uint8    // tile index\n}\n\n// Count returns number of objects at the current tile.\nfunc (t Tile[T]) Count() (count int) {\n\tt.data.Lock()\n\tdefer t.data.Unlock()\n\tfor _, idx := range t.data.state {\n\t\tif idx == uint8(t.idx) {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn\n}\n\n// Point returns the point of the tile\nfunc (t Tile[T]) Point() Point {\n\treturn pointOf(t.data.point, t.idx)\n}\n\n// Value reads the tile information\nfunc (t Tile[T]) Value() Value {\n\treturn t.data.tileAt(t.idx)\n}\n\n// Range iterates over all of the objects in the set\nfunc (t Tile[T]) Range(fn func(T) error) error {\n\tt.data.Lock()\n\tdefer t.data.Unlock()\n\tfor v, idx := range t.data.state {\n\t\tif idx == uint8(t.idx) {\n\t\t\tif err := fn(v); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// IsObserved returns whether the tile is observed or not\nfunc (t Tile[T]) IsObserved() bool {\n\treturn t.data.IsObserved()\n}\n\n// Observers iterates over all views observing this tile\nfunc (t Tile[T]) Observers(fn func(view Observer[T])) {\n\tif !t.data.IsObserved() {\n\t\treturn\n\t}\n\n\tt.grid.observers.Each1(func(sub Observer[T]) {\n\t\tif sub.Viewport().Contains(t.Point()) {\n\t\t\tfn(sub)\n\t\t}\n\t}, t.data.point)\n}\n\n// Add adds object to the set\nfunc (t Tile[T]) Add(v T) {\n\tvalue := t.data.addObject(t.idx, v)\n\n\t// If observed, notify the observers of the tile\n\tif t.data.IsObserved() {\n\t\tat := t.Point()\n\t\tt.grid.observers.Notify1(&Update[T]{\n\t\t\tOld: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: value,\n\t\t\t},\n\t\t\tNew: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: value,\n\t\t\t},\n\t\t\tAdd: v,\n\t\t}, t.data.point)\n\t}\n}\n\n// Del removes the object from the set\nfunc (t Tile[T]) Del(v T) {\n\tvalue := t.data.delObject(t.idx, v)\n\n\t// If observed, notify the observers of the tile\n\tif t.data.IsObserved() {\n\t\tat := t.Point()\n\t\tt.grid.observers.Notify1(&Update[T]{\n\t\t\tOld: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: value,\n\t\t\t},\n\t\t\tNew: ValueAt{\n\t\t\t\tPoint: at,\n\t\t\t\tValue: value,\n\t\t\t},\n\t\t\tDel: v,\n\t\t}, t.data.point)\n\t}\n}\n\n// Move moves an object from the current tile to the destination tile.\nfunc (t Tile[T]) Move(v T, dst Point) bool {\n\td, ok := t.grid.At(dst.X, dst.Y)\n\tif !ok {\n\t\treturn false\n\t}\n\n\t// Move the object from the source to the destination\n\ttv := t.data.delObject(d.idx, v)\n\tdv := d.data.addObject(d.idx, v)\n\tif !t.data.IsObserved() && !d.data.IsObserved() {\n\t\treturn true\n\t}\n\n\t// Prepare the update notification\n\tupdate := &Update[T]{\n\t\tOld: ValueAt{\n\t\t\tPoint: t.Point(),\n\t\t\tValue: tv,\n\t\t},\n\t\tNew: ValueAt{\n\t\t\tPoint: d.Point(),\n\t\t\tValue: dv,\n\t\t},\n\t\tDel: v,\n\t\tAdd: v,\n\t}\n\n\tswitch {\n\tcase t.data == d.data || !d.data.IsObserved():\n\t\tt.grid.observers.Notify1(update, t.data.point)\n\tcase !t.data.IsObserved():\n\t\tt.grid.observers.Notify1(update, d.data.point)\n\tdefault:\n\t\tt.grid.observers.Notify2(update, [2]Point{\n\t\t\tt.data.point,\n\t\t\td.data.point,\n\t\t})\n\t}\n\treturn true\n}\n\n// Write updates the entire tile value.\nfunc (t Tile[T]) Write(tile Value) {\n\tt.data.writeTile(t.grid, t.idx, tile)\n}\n\n// Merge atomically merges the tile by applying a merging function.\nfunc (t Tile[T]) Merge(merge func(Value) Value) Value {\n\treturn t.data.mergeTile(t.grid, t.idx, merge)\n}\n\n// Mask updates the bits of tile. The bits are specified by the mask. The bits\n// that need to be updated should be flipped on in the mask.\nfunc (t Tile[T]) Mask(tile, mask Value) Value {\n\treturn t.data.mergeTile(t.grid, t.idx, func(value Value) Value {\n\t\treturn (value &^ mask) | (tile & mask)\n\t})\n}\n\n// pointOf returns the point given an index\nfunc pointOf(page Point, idx uint8) Point {\n\treturn Point{\n\t\tX: page.X + int16(idx)%3,\n\t\tY: page.Y + int16(idx)/3,\n\t}\n}\n"
  },
  {
    "path": "grid_test.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE file in the project root for details.\r\n\r\npackage tile\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"io\"\r\n\t\"sync\"\r\n\t\"testing\"\r\n\t\"unsafe\"\r\n\r\n\t\"github.com/stretchr/testify/assert\"\r\n)\r\n\r\n/*\r\ncpu: 13th Gen Intel(R) Core(TM) i7-13700K\r\nBenchmarkGrid/each-24         \t    1452\t    \t830268 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkGrid/neighbors-24    \t121583491\t         9.861 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkGrid/within-24       \t   49360\t     \t 24477 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkGrid/at-24           \t687659378\t         1.741 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkGrid/write-24        \t191272338\t         6.307 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkGrid/merge-24        \t162536985\t         7.332 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkGrid/mask-24         \t158258084\t         7.601 ns/op\t       0 B/op\t       0 allocs/op\r\n*/\r\nfunc BenchmarkGrid(b *testing.B) {\r\n\tvar d Tile[uint32]\r\n\tvar p Point\r\n\tdefer assert.NotNil(b, d)\r\n\tm := NewGridOf[uint32](768, 768)\r\n\r\n\tb.Run(\"each\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tm.Each(func(point Point, tile Tile[uint32]) {\r\n\t\t\t\tp = point\r\n\t\t\t\td = tile\r\n\t\t\t})\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"neighbors\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tm.Neighbors(300, 300, func(point Point, tile Tile[uint32]) {\r\n\t\t\t\tp = point\r\n\t\t\t\td = tile\r\n\t\t\t})\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"within\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tm.Within(At(100, 100), At(200, 200), func(point Point, tile Tile[uint32]) {\r\n\t\t\t\tp = point\r\n\t\t\t\td = tile\r\n\t\t\t})\r\n\t\t}\r\n\t})\r\n\r\n\tassert.NotZero(b, p.X)\r\n\tb.Run(\"at\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\td, _ = m.At(100, 100)\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"write\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tm.WriteAt(100, 100, Value(0))\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"merge\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tm.MergeAt(100, 100, func(v Value) Value {\r\n\t\t\t\tv += 1\r\n\t\t\t\treturn v\r\n\t\t\t})\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"mask\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tm.MaskAt(100, 100, Value(0), Value(1))\r\n\t\t}\r\n\t})\r\n}\r\n\r\n/*\r\ncpu: 13th Gen Intel(R) Core(TM) i7-13700K\r\nBenchmarkState/range-24         \t17017800\t        71.14 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkState/add-24           \t72639224\t        16.32 ns/op\t       0 B/op\t       0 allocs/op\r\nBenchmarkState/del-24           \t82469125\t        13.65 ns/op\t       0 B/op\t       0 allocs/op\r\n*/\r\nfunc BenchmarkState(b *testing.B) {\r\n\tm := NewGridOf[int](768, 768)\r\n\tm.Each(func(p Point, c Tile[int]) {\r\n\t\tfor i := 0; i < 10; i++ {\r\n\t\t\tc.Add(i)\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"range\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tcursor, _ := m.At(100, 100)\r\n\t\t\tcursor.Range(func(v int) error {\r\n\t\t\t\treturn nil\r\n\t\t\t})\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"add\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tcursor, _ := m.At(100, 100)\r\n\t\t\tcursor.Add(100)\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"del\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tcursor, _ := m.At(100, 100)\r\n\t\t\tcursor.Del(100)\r\n\t\t}\r\n\t})\r\n}\r\n\r\nfunc TestPageSize(t *testing.T) {\r\n\tassert.Equal(t, 8, int(unsafe.Sizeof(map[uintptr]Point{})))\r\n\tassert.Equal(t, 64, int(unsafe.Sizeof(page[string]{})))\r\n\tassert.Equal(t, 36, int(unsafe.Sizeof([9]Value{})))\r\n}\r\n\r\nfunc TestWithin(t *testing.T) {\r\n\tm := NewGrid(9, 9)\r\n\r\n\tvar path []string\r\n\tm.Within(At(1, 1), At(5, 5), func(p Point, tile Tile[string]) {\r\n\t\tpath = append(path, p.String())\r\n\t})\r\n\tassert.Equal(t, 16, len(path))\r\n\tassert.ElementsMatch(t, []string{\r\n\t\t\"1,1\", \"2,1\", \"1,2\", \"2,2\",\r\n\t\t\"3,1\", \"4,1\", \"3,2\", \"4,2\",\r\n\t\t\"1,3\", \"2,3\", \"1,4\", \"2,4\",\r\n\t\t\"3,3\", \"4,3\", \"3,4\", \"4,4\",\r\n\t}, path)\r\n}\r\n\r\nfunc TestWithinCorner(t *testing.T) {\r\n\tm := NewGrid(9, 9)\r\n\r\n\tvar path []string\r\n\tm.Within(At(7, 6), At(10, 10), func(p Point, tile Tile[string]) {\r\n\t\tpath = append(path, p.String())\r\n\t})\r\n\tassert.Equal(t, 6, len(path))\r\n\tassert.ElementsMatch(t, []string{\r\n\t\t\"7,6\", \"8,6\", \"7,7\",\r\n\t\t\"8,7\", \"7,8\", \"8,8\",\r\n\t}, path)\r\n}\r\n\r\nfunc TestWithinXY(t *testing.T) {\r\n\tassert.False(t, At(4, 8).WithinRect(NewRect(1, 6, 4, 10)))\r\n}\r\n\r\nfunc TestWithinOneSide(t *testing.T) {\r\n\tm := NewGrid(9, 9)\r\n\r\n\tvar path []string\r\n\tm.Within(At(1, 6), At(4, 10), func(p Point, tile Tile[string]) {\r\n\t\tpath = append(path, p.String())\r\n\t})\r\n\tassert.Equal(t, 9, len(path))\r\n\tassert.ElementsMatch(t, []string{\r\n\t\t\"1,6\", \"2,6\", \"3,6\",\r\n\t\t\"1,7\", \"2,7\", \"3,7\",\r\n\t\t\"1,8\", \"2,8\", \"3,8\",\r\n\t}, path)\r\n}\r\n\r\nfunc TestWithinInvalid(t *testing.T) {\r\n\tm := NewGrid(9, 9)\r\n\tcount := 0\r\n\tm.Within(At(10, 10), At(20, 20), func(p Point, tile Tile[string]) {\r\n\t\tcount++\r\n\t})\r\n\tassert.Equal(t, 0, count)\r\n}\r\n\r\nfunc TestEach(t *testing.T) {\r\n\tm := NewGrid(9, 9)\r\n\r\n\tvar path []string\r\n\tm.Each(func(p Point, tile Tile[string]) {\r\n\t\tpath = append(path, p.String())\r\n\t})\r\n\tassert.Equal(t, 81, len(path))\r\n\tassert.ElementsMatch(t, []string{\r\n\t\t\"0,0\", \"1,0\", \"2,0\", \"0,1\", \"1,1\", \"2,1\", \"0,2\", \"1,2\", \"2,2\",\r\n\t\t\"0,3\", \"1,3\", \"2,3\", \"0,4\", \"1,4\", \"2,4\", \"0,5\", \"1,5\", \"2,5\",\r\n\t\t\"0,6\", \"1,6\", \"2,6\", \"0,7\", \"1,7\", \"2,7\", \"0,8\", \"1,8\", \"2,8\",\r\n\t\t\"3,0\", \"4,0\", \"5,0\", \"3,1\", \"4,1\", \"5,1\", \"3,2\", \"4,2\", \"5,2\",\r\n\t\t\"3,3\", \"4,3\", \"5,3\", \"3,4\", \"4,4\", \"5,4\", \"3,5\", \"4,5\", \"5,5\",\r\n\t\t\"3,6\", \"4,6\", \"5,6\", \"3,7\", \"4,7\", \"5,7\", \"3,8\", \"4,8\", \"5,8\",\r\n\t\t\"6,0\", \"7,0\", \"8,0\", \"6,1\", \"7,1\", \"8,1\", \"6,2\", \"7,2\", \"8,2\",\r\n\t\t\"6,3\", \"7,3\", \"8,3\", \"6,4\", \"7,4\", \"8,4\", \"6,5\", \"7,5\", \"8,5\",\r\n\t\t\"6,6\", \"7,6\", \"8,6\", \"6,7\", \"7,7\", \"8,7\", \"6,8\", \"7,8\", \"8,8\",\r\n\t}, path)\r\n}\r\n\r\nfunc TestNeighbors(t *testing.T) {\r\n\ttests := []struct {\r\n\t\tx, y   int16\r\n\t\texpect []string\r\n\t}{\r\n\t\t{x: 0, y: 0, expect: []string{\"1,0\", \"0,1\"}},\r\n\t\t{x: 1, y: 0, expect: []string{\"2,0\", \"1,1\", \"0,0\"}},\r\n\t\t{x: 1, y: 1, expect: []string{\"1,0\", \"2,1\", \"1,2\", \"0,1\"}},\r\n\t\t{x: 2, y: 2, expect: []string{\"2,1\", \"3,2\", \"2,3\", \"1,2\"}},\r\n\t\t{x: 8, y: 8, expect: []string{\"8,7\", \"7,8\"}},\r\n\t}\r\n\r\n\t// Create a 9x9 map with labeled tiles\r\n\tm := NewGrid(9, 9)\r\n\tm.Each(func(p Point, tile Tile[string]) {\r\n\t\tm.WriteAt(p.X, p.Y, Value(p.Integer()))\r\n\t})\r\n\r\n\t// Run all the tests\r\n\tfor _, tc := range tests {\r\n\t\tvar out []string\r\n\t\tm.Neighbors(tc.x, tc.y, func(_ Point, tile Tile[string]) {\r\n\t\t\tloc := unpackPoint(uint32(tile.Value()))\r\n\t\t\tout = append(out, loc.String())\r\n\t\t})\r\n\t\tassert.ElementsMatch(t, tc.expect, out)\r\n\t}\r\n}\r\n\r\nfunc TestAt(t *testing.T) {\r\n\r\n\t// Create a 9x9 map with labeled tiles\r\n\tm := NewGrid(9, 9)\r\n\tm.Each(func(p Point, tile Tile[string]) {\r\n\t\tm.WriteAt(p.X, p.Y, Value(p.Integer()))\r\n\t})\r\n\r\n\t// Make sure our At() and the position matches\r\n\tm.Each(func(p Point, tile Tile[string]) {\r\n\t\tat, _ := m.At(p.X, p.Y)\r\n\t\tassert.Equal(t, p.String(), unpackPoint(uint32(at.Value())).String())\r\n\t})\r\n\r\n\t// Make sure that points match\r\n\tfor y := int16(0); y < 9; y++ {\r\n\t\tfor x := int16(0); x < 9; x++ {\r\n\t\t\tat, _ := m.At(x, y)\r\n\t\t\tassert.Equal(t, At(x, y).String(), unpackPoint(uint32(at.Value())).String())\r\n\t\t}\r\n\t}\r\n}\r\n\r\nfunc TestUpdate(t *testing.T) {\r\n\r\n\t// Create a 9x9 map with labeled tiles\r\n\tm := NewGrid(9, 9)\r\n\ti := 0\r\n\tm.Each(func(p Point, _ Tile[string]) {\r\n\t\ti++\r\n\t\tm.WriteAt(p.X, p.Y, Value(i))\r\n\t})\r\n\r\n\t// Assert the update\r\n\tcursor, _ := m.At(8, 8)\r\n\tassert.Equal(t, 81, int(cursor.Value()))\r\n\r\n\t// 81 = 0b01010001\r\n\tdelta := Value(0b00101110) // change last 2 bits and should ignore other bits\r\n\tm.MaskAt(8, 8, delta, Value(0b00000011))\r\n\r\n\t// original: 0101 0001\r\n\t// delta:    0010 1110\r\n\t// mask:     0000 0011\r\n\t// result:   0101 0010\r\n\tcursor, _ = m.At(8, 8)\r\n\tassert.Equal(t, 0b01010010, int(cursor.Value()))\r\n}\r\n\r\nfunc TestState(t *testing.T) {\r\n\tm := NewGrid(9, 9)\r\n\tm.Each(func(p Point, c Tile[string]) {\r\n\t\tc.Add(p.String())\r\n\t\tc.Add(p.String()) // duplicate\r\n\t})\r\n\r\n\tm.Each(func(p Point, c Tile[string]) {\r\n\t\tassert.Equal(t, 1, c.Count())\r\n\t\tassert.NoError(t, c.Range(func(s string) error {\r\n\t\t\tassert.Equal(t, p.String(), s)\r\n\t\t\treturn nil\r\n\t\t}))\r\n\r\n\t\tc.Del(p.String())\r\n\t\tassert.Equal(t, 0, c.Count())\r\n\t})\r\n}\r\n\r\nfunc TestStateRangeErr(t *testing.T) {\r\n\tm := NewGrid(9, 9)\r\n\tm.Each(func(p Point, c Tile[string]) {\r\n\t\tc.Add(p.String())\r\n\t})\r\n\r\n\tm.Each(func(p Point, c Tile[string]) {\r\n\t\tassert.Error(t, c.Range(func(s string) error {\r\n\t\t\treturn io.EOF\r\n\t\t}))\r\n\t})\r\n}\r\n\r\nfunc TestPointOf(t *testing.T) {\r\n\ttruthTable := func(x, y int16, idx uint8) (int16, int16) {\r\n\t\tswitch idx {\r\n\t\tcase 0:\r\n\t\t\treturn x, y\r\n\t\tcase 1:\r\n\t\t\treturn x + 1, y\r\n\t\tcase 2:\r\n\t\t\treturn x + 2, y\r\n\t\tcase 3:\r\n\t\t\treturn x, y + 1\r\n\t\tcase 4:\r\n\t\t\treturn x + 1, y + 1\r\n\t\tcase 5:\r\n\t\t\treturn x + 2, y + 1\r\n\t\tcase 6:\r\n\t\t\treturn x, y + 2\r\n\t\tcase 7:\r\n\t\t\treturn x + 1, y + 2\r\n\t\tcase 8:\r\n\t\t\treturn x + 2, y + 2\r\n\t\tdefault:\r\n\t\t\treturn x, y\r\n\t\t}\r\n\t}\r\n\r\n\tfor i := 0; i < 9; i++ {\r\n\t\tat := pointOf(At(0, 0), uint8(i))\r\n\t\tx, y := truthTable(0, 0, uint8(i))\r\n\t\tassert.Equal(t, x, at.X, fmt.Sprintf(\"idx=%v\", i))\r\n\t\tassert.Equal(t, y, at.Y, fmt.Sprintf(\"idx=%v\", i))\r\n\t}\r\n}\r\n\r\nfunc TestConcurrentMerge(t *testing.T) {\r\n\tconst count = 10000\r\n\tvar wg sync.WaitGroup\r\n\twg.Add(count)\r\n\r\n\tm := NewGrid(9, 9)\r\n\tfor i := 0; i < count; i++ {\r\n\t\tgo func() {\r\n\t\t\tm.MergeAt(1, 1, func(v Value) Value {\r\n\t\t\t\tv += 1\r\n\t\t\t\treturn v\r\n\t\t\t})\r\n\t\t\twg.Done()\r\n\t\t}()\r\n\t}\r\n\r\n\twg.Wait()\r\n\ttile, ok := m.At(1, 1)\r\n\tassert.True(t, ok)\r\n\tassert.Equal(t, uint32(count), tile.Value())\r\n}\r\n"
  },
  {
    "path": "path.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for details.\n\npackage tile\n\nimport (\n\t\"math\"\n\t\"math/bits\"\n\t\"sync\"\n\n\t\"github.com/kelindar/intmap\"\n)\n\ntype costFn = func(Value) uint16\n\n// Edge represents an edge of the path\ntype edge struct {\n\tPoint\n\tCost uint32\n}\n\n// Around performs a breadth first search around a point.\nfunc (m *Grid[T]) Around(from Point, distance uint32, costOf costFn, fn func(Point, Tile[T])) {\n\tstart, ok := m.At(from.X, from.Y)\n\tif !ok {\n\t\treturn\n\t}\n\n\tfn(from, start)\n\n\t// For pre-allocating, we use πr2 since BFS will result in a approximation\n\t// of a circle, in the worst case.\n\tmaxArea := int(math.Ceil(math.Pi * float64(distance*distance)))\n\n\t// Acquire a frontier heap for search\n\tstate := acquire(maxArea)\n\tfrontier := state.frontier\n\treached := state.edges\n\tdefer release(state)\n\n\tfrontier.Push(from.Integer(), 0)\n\treached.Store(from.Integer(), 0)\n\tfor !frontier.IsEmpty() {\n\t\tpCurr := frontier.Pop()\n\t\tcurrent := unpackPoint(pCurr)\n\n\t\t// Get all of the neighbors\n\t\tm.Neighbors(current.X, current.Y, func(next Point, nextTile Tile[T]) {\n\t\t\tif d := from.DistanceTo(next); d > distance {\n\t\t\t\treturn // Too far\n\t\t\t}\n\n\t\t\tif cost := costOf(nextTile.Value()); cost == 0 {\n\t\t\t\treturn // Blocked tile, ignore completely\n\t\t\t}\n\n\t\t\t// Add to the search queue\n\t\t\tpNext := next.Integer()\n\t\t\tif _, ok := reached.Load(pNext); !ok {\n\t\t\t\tfrontier.Push(pNext, 1)\n\t\t\t\treached.Store(pNext, 1)\n\t\t\t\tfn(next, nextTile)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Path calculates a short path and the distance between the two locations\nfunc (m *Grid[T]) Path(from, to Point, costOf costFn) ([]Point, int, bool) {\n\tdistance := float64(from.DistanceTo(to))\n\tmaxArea := int(math.Ceil(math.Pi * float64(distance*distance)))\n\n\t// For pre-allocating, we use πr2 since BFS will result in a approximation\n\t// of a circle, in the worst case.\n\tstate := acquire(maxArea)\n\tedges := state.edges\n\tfrontier := state.frontier\n\tdefer release(state)\n\n\tfrontier.Push(from.Integer(), 0)\n\tedges.Store(from.Integer(), encode(0, Direction(0))) // Starting point has no direction\n\n\tfor !frontier.IsEmpty() {\n\t\tpCurr := frontier.Pop()\n\t\tcurrent := unpackPoint(pCurr)\n\n\t\t// Decode the cost to reach the current point\n\t\tcurrentEncoded, _ := edges.Load(pCurr)\n\t\tcurrentCost, _ := decode(currentEncoded)\n\n\t\t// Check if we've reached the destination\n\t\tif current.Equal(to) {\n\n\t\t\t// Reconstruct the path\n\t\t\tpath := make([]Point, 0, 64)\n\t\t\tpath = append(path, current)\n\t\t\tfor !current.Equal(from) {\n\t\t\t\tcurrentEncoded, _ := edges.Load(current.Integer())\n\t\t\t\t_, dir := decode(currentEncoded)\n\t\t\t\tcurrent = current.Move(oppositeDirection(dir))\n\t\t\t\tpath = append(path, current)\n\t\t\t}\n\n\t\t\t// Reverse the path to get from source to destination\n\t\t\tfor i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {\n\t\t\t\tpath[i], path[j] = path[j], path[i]\n\t\t\t}\n\n\t\t\treturn path, int(currentCost), true\n\t\t}\n\n\t\t// Explore neighbors\n\t\tm.Neighbors(current.X, current.Y, func(next Point, nextTile Tile[T]) {\n\t\t\tcNext := costOf(nextTile.Value())\n\t\t\tif cNext == 0 {\n\t\t\t\treturn // Blocked tile\n\t\t\t}\n\n\t\t\tnextCost := currentCost + uint32(cNext)\n\t\t\tpNext := next.Integer()\n\n\t\t\texistingEncoded, visited := edges.Load(pNext)\n\t\t\texistingCost, _ := decode(existingEncoded)\n\n\t\t\t// If we haven't visited this node or we found a better path\n\t\t\tif !visited || nextCost < existingCost {\n\t\t\t\tangle := angleOf(current, next)\n\t\t\t\tpriority := nextCost + next.DistanceTo(to)\n\n\t\t\t\t// Store the edge and push to the frontier\n\t\t\t\tedges.Store(pNext, encode(nextCost, angle))\n\t\t\t\tfrontier.Push(pNext, priority)\n\t\t\t}\n\t\t})\n\t}\n\n\treturn nil, 0, false\n}\n\n// encode packs the cost and direction into a uint32\nfunc encode(cost uint32, dir Direction) uint32 {\n\treturn (cost << 4) | uint32(dir&0xF)\n}\n\n// decode unpacks the cost and direction from a uint32\nfunc decode(value uint32) (cost uint32, dir Direction) {\n\tcost = value >> 4\n\tdir = Direction(value & 0xF)\n\treturn\n}\n\n// -----------------------------------------------------------------------------\n\ntype pathfinder struct {\n\tedges    *intmap.Map\n\tfrontier *frontier\n}\n\nvar pathfinders = sync.Pool{\n\tNew: func() any {\n\t\treturn &pathfinder{\n\t\t\tedges:    intmap.NewWithFill(32, .99),\n\t\t\tfrontier: newFrontier(),\n\t\t}\n\t},\n}\n\n// Acquires a new instance of a pathfinding state\nfunc acquire(capacity int) *pathfinder {\n\tv := pathfinders.Get().(*pathfinder)\n\tif v.edges.Capacity() < capacity {\n\t\tv.edges = intmap.NewWithFill(capacity, .99)\n\t}\n\n\treturn v\n}\n\n// release releases a pathfinding state back to the pool\nfunc release(v *pathfinder) {\n\tv.edges.Clear()\n\tv.frontier.Reset()\n\tpathfinders.Put(v)\n}\n\n// -----------------------------------------------------------------------------\n\n// frontier is a priority queue implementation that uses buckets to store\n// elements. Original implementation by Iskander Sharipov (https://github.com/quasilyte/pathing)\ntype frontier struct {\n\tbuckets [64][]uint32\n\tmask    uint64\n}\n\n// newFrontier creates a new frontier priority queue\nfunc newFrontier() *frontier {\n\th := &frontier{}\n\tfor i := range &h.buckets {\n\t\th.buckets[i] = make([]uint32, 0, 16)\n\t}\n\treturn h\n}\n\nfunc (q *frontier) Reset() {\n\tbuckets := &q.buckets\n\n\t// Reslice storage slices back.\n\t// To avoid traversing all len(q.buckets),\n\t// we have some offset to skip uninteresting (already empty) buckets.\n\t// We also stop when mask is 0 meaning all remaining buckets are empty too.\n\t// In other words, it would only touch slices between min and max non-empty priorities.\n\tmask := q.mask\n\toffset := uint(bits.TrailingZeros64(mask))\n\tmask >>= offset\n\ti := offset\n\tfor mask != 0 {\n\t\tif i < uint(len(buckets)) {\n\t\t\tbuckets[i] = buckets[i][:0]\n\t\t}\n\t\tmask >>= 1\n\t\ti++\n\t}\n\n\tq.mask = 0\n}\n\nfunc (q *frontier) IsEmpty() bool {\n\treturn q.mask == 0\n}\n\nfunc (q *frontier) Push(value, priority uint32) {\n\t// No bound checks since compiler knows that i will never exceed 64.\n\t// We also get a cool truncation of values above 64 to store them\n\t// in our biggest bucket.\n\ti := priority & 0b111111\n\tq.buckets[i] = append(q.buckets[i], value)\n\tq.mask |= 1 << i\n}\n\nfunc (q *frontier) Pop() uint32 {\n\tbuckets := &q.buckets\n\n\t// Using uints here and explicit len check to avoid the\n\t// implicitly inserted bound check.\n\ti := uint(bits.TrailingZeros64(q.mask))\n\tif i < uint(len(buckets)) {\n\t\te := buckets[i][len(buckets[i])-1]\n\t\tbuckets[i] = buckets[i][:len(buckets[i])-1]\n\t\tif len(buckets[i]) == 0 {\n\t\t\tq.mask &^= 1 << i\n\t\t}\n\t\treturn e\n\t}\n\n\t// A queue is empty\n\treturn 0\n}\n"
  },
  {
    "path": "path_test.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for details.\n\npackage tile\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/png\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPath(t *testing.T) {\n\tm := mapFrom(\"9x9.png\")\n\tpath, dist, found := m.Path(At(1, 1), At(7, 7), costOf)\n\tassert.Equal(t, `\n.........\n.x  .   .\n.x ... ..\n.xxx . ..\n...x .  .\n.  xxx  .\n.....x...\n.    xxx.\n.........`, plotPath(m, path))\n\n\tfmt.Println(plotPath(m, path))\n\tassert.Equal(t, 12, dist)\n\tassert.True(t, found)\n}\n\nfunc TestPathTiny(t *testing.T) {\n\tm := NewGrid(6, 6)\n\tpath, dist, found := m.Path(At(0, 0), At(5, 5), costOf)\n\tassert.Equal(t, `\nx     \nx     \nx     \nx     \nx     \nxxxxxx`, plotPath(m, path))\n\tassert.Equal(t, 10, dist)\n\tassert.True(t, found)\n}\n\nfunc TestDraw(t *testing.T) {\n\tm := mapFrom(\"9x9.png\")\n\tout := drawGrid(m, NewRect(0, 0, 0, 0))\n\tassert.NotNil(t, out)\n}\n\n/*\nBenchmarkPath/9x9-24         \t 2856020\t       423.0 ns/op\t     256 B/op\t       1 allocs/op\nBenchmarkPath/300x300-24     \t    1167\t   1006143 ns/op\t    3845 B/op\t       4 allocs/op\nBenchmarkPath/381x381-24     \t    3150\t    371478 ns/op\t   12629 B/op\t       5 allocs/op\nBenchmarkPath/384x384-24     \t    3178\t    374982 ns/op\t    7298 B/op\t       5 allocs/op\nBenchmarkPath/3069x3069-24   \t     787\t   1459683 ns/op\t  106188 B/op\t       7 allocs/op\nBenchmarkPath/3072x3072-24   \t     799\t   1552230 ns/op\t  104906 B/op\t       7 allocs/op\nBenchmarkPath/6144x6144-24   \t    3099\t    381935 ns/op\t   12716 B/op\t       5 allocs/op\n*/\nfunc BenchmarkPath(b *testing.B) {\n\tb.Run(\"9x9\", func(b *testing.B) {\n\t\tm := mapFrom(\"9x9.png\")\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Path(At(1, 1), At(7, 7), costOf)\n\t\t}\n\t})\n\n\tb.Run(\"300x300\", func(b *testing.B) {\n\t\tm := mapFrom(\"300x300.png\")\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Path(At(115, 20), At(160, 270), costOf)\n\t\t}\n\t})\n\n\tb.Run(\"381x381\", func(b *testing.B) {\n\t\tm := NewGrid(381, 381)\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Path(At(0, 0), At(380, 380), costOf)\n\t\t}\n\t})\n\n\tb.Run(\"384x384\", func(b *testing.B) {\n\t\tm := NewGrid(384, 384)\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Path(At(0, 0), At(380, 380), costOf)\n\t\t}\n\t})\n\n\tb.Run(\"3069x3069\", func(b *testing.B) {\n\t\tm := NewGrid(3069, 3069)\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Path(At(0, 0), At(700, 700), costOf)\n\t\t}\n\t})\n\n\tb.Run(\"3072x3072\", func(b *testing.B) {\n\t\tm := NewGrid(3072, 3072)\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Path(At(0, 0), At(700, 700), costOf)\n\t\t}\n\t})\n\n\tb.Run(\"6144x6144\", func(b *testing.B) {\n\t\tm := NewGrid(6144, 6144)\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Path(At(0, 0), At(380, 380), costOf)\n\t\t}\n\t})\n}\n\n/*\ncpu: 13th Gen Intel(R) Core(TM) i7-13700K\nBenchmarkAround/3r-24 \t 2080566\t     562.7 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkAround/5r-24 \t  885582\t      1358 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkAround/10r-24    300672\t      3953 ns/op\t       0 B/op\t       0 allocs/op\n*/\nfunc BenchmarkAround(b *testing.B) {\n\tm := mapFrom(\"300x300.png\")\n\tb.Run(\"3r\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Around(At(115, 20), 3, costOf, func(_ Point, _ Tile[string]) {})\n\t\t}\n\t})\n\n\tb.Run(\"5r\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Around(At(115, 20), 5, costOf, func(_ Point, _ Tile[string]) {})\n\t\t}\n\t})\n\n\tb.Run(\"10r\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tm.Around(At(115, 20), 10, costOf, func(_ Point, _ Tile[string]) {})\n\t\t}\n\t})\n}\n\nfunc TestAround(t *testing.T) {\n\tm := mapFrom(\"9x9.png\")\n\n\tfor i := 0; i < 3; i++ {\n\t\tvar path []string\n\t\tm.Around(At(2, 2), 3, costOf, func(p Point, tile Tile[string]) {\n\t\t\tpath = append(path, p.String())\n\t\t})\n\t\tassert.Equal(t, 10, len(path))\n\t\tassert.ElementsMatch(t, []string{\n\t\t\t\"2,2\", \"2,1\", \"2,3\", \"1,2\", \"3,1\",\n\t\t\t\"1,1\", \"1,3\", \"3,3\", \"4,3\", \"3,4\",\n\t\t}, path)\n\t}\n}\n\nfunc TestAroundMiss(t *testing.T) {\n\tm := mapFrom(\"9x9.png\")\n\tm.Around(At(20, 20), 3, costOf, func(p Point, tile Tile[string]) {\n\t\tt.Fail()\n\t})\n}\n\n/*\ncpu: 13th Gen Intel(R) Core(TM) i7-13700K\nBenchmarkHeap-24    \t  240228\t      5076 ns/op\t    6016 B/op\t      68 allocs/op\n*/\nfunc BenchmarkHeap(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\th := newFrontier()\n\t\tfor j := 0; j < 128; j++ {\n\t\t\th.Push(rand(j), 1)\n\t\t}\n\t\tfor j := 0; j < 128*10; j++ {\n\t\t\th.Push(rand(j), 1)\n\t\t\th.Pop()\n\t\t}\n\t}\n}\n\n// very fast semi-random function\nfunc rand(i int) uint32 {\n\ti = i + 10000\n\ti = i ^ (i << 16)\n\ti = (i >> 5) ^ i\n\treturn uint32(i & 0xFF)\n}\n\n// -----------------------------------------------------------------------------\n\n// Cost estimation function\nfunc costOf(tile Value) uint16 {\n\tif (tile)&1 != 0 {\n\t\treturn 0 // Blocked\n\t}\n\treturn 1\n}\n\n// mapFrom creates a map from ASCII string\nfunc mapFrom(name string) *Grid[string] {\n\tf, err := os.Open(\"fixtures/\" + name)\n\tdefer f.Close()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Decode the image\n\timg, err := png.Decode(f)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tm := NewGrid(int16(img.Bounds().Dx()), int16(img.Bounds().Dy()))\n\tfor y := int16(0); y < m.Size.Y; y++ {\n\t\tfor x := int16(0); x < m.Size.X; x++ {\n\t\t\t//fmt.Printf(\"%+v %T\\n\", img.At(int(x), int(y)), img.At(int(x), int(y)))\n\t\t\tv := img.At(int(x), int(y)).(color.RGBA)\n\t\t\tswitch v.R {\n\t\t\tcase 255:\n\t\t\tcase 0:\n\t\t\t\tm.WriteAt(x, y, Value(0xff))\n\t\t\t}\n\n\t\t}\n\t}\n\treturn m\n}\n\n// plotPath plots the path on ASCII map\nfunc plotPath(m *Grid[string], path []Point) string {\n\tout := make([][]byte, m.Size.Y)\n\tfor i := range out {\n\t\tout[i] = make([]byte, m.Size.X)\n\t}\n\n\tm.Each(func(l Point, tile Tile[string]) {\n\t\t//println(l.String(), int(tile[0]))\n\t\tswitch {\n\t\tcase pointInPath(l, path):\n\t\t\tout[l.Y][l.X] = 'x'\n\t\tcase tile.Value()&1 != 0:\n\t\t\tout[l.Y][l.X] = '.'\n\t\tdefault:\n\t\t\tout[l.Y][l.X] = ' '\n\t\t}\n\t})\n\n\tvar sb strings.Builder\n\tfor _, line := range out {\n\t\tsb.WriteByte('\\n')\n\t\tsb.WriteString(string(line))\n\t}\n\treturn sb.String()\n}\n\n// pointInPath returns whether a point is part of a path or not\nfunc pointInPath(point Point, path []Point) bool {\n\tfor _, p := range path {\n\t\tif p.Equal(point) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// draw converts the map to a black and white image for debugging purposes.\nfunc drawGrid(m *Grid[string], rect Rect) image.Image {\n\tif rect.Max.X == 0 || rect.Max.Y == 0 {\n\t\trect = NewRect(0, 0, m.Size.X, m.Size.Y)\n\t}\n\n\tsize := rect.Size()\n\toutput := image.NewRGBA(image.Rect(0, 0, int(size.X), int(size.Y)))\n\tm.Within(rect.Min, rect.Max, func(p Point, tile Tile[string]) {\n\t\ta := uint8(255)\n\t\tif tile.Value() == 1 {\n\t\t\ta = 0\n\t\t}\n\n\t\toutput.SetRGBA(int(p.X), int(p.Y), color.RGBA{a, a, a, 255})\n\t})\n\treturn output\n}\n"
  },
  {
    "path": "point.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for details.\n\npackage tile\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\n// -----------------------------------------------------------------------------\n\n// Point represents a 2D coordinate.\ntype Point struct {\n\tX int16 // X coordinate\n\tY int16 // Y coordinate\n}\n\nfunc unpackPoint(v uint32) Point {\n\treturn At(int16(v>>16), int16(v))\n}\n\n// At creates a new point at a specified x,y coordinate.\nfunc At(x, y int16) Point {\n\treturn Point{X: x, Y: y}\n}\n\n// String returns string representation of a point.\nfunc (p Point) String() string {\n\treturn fmt.Sprintf(\"%v,%v\", p.X, p.Y)\n}\n\n// Integer returns a packed 32-bit integer representation of a point.\nfunc (p Point) Integer() uint32 {\n\treturn (uint32(p.X) << 16) | (uint32(p.Y) & 0xffff)\n}\n\n// Equal compares two points and returns true if they are equal.\nfunc (p Point) Equal(other Point) bool {\n\treturn p.X == other.X && p.Y == other.Y\n}\n\n// Add adds two points together.\nfunc (p Point) Add(p2 Point) Point {\n\treturn Point{p.X + p2.X, p.Y + p2.Y}\n}\n\n// Subtract subtracts the second point from the first.\nfunc (p Point) Subtract(p2 Point) Point {\n\treturn Point{p.X - p2.X, p.Y - p2.Y}\n}\n\n// Multiply multiplies two points together.\nfunc (p Point) Multiply(p2 Point) Point {\n\treturn Point{p.X * p2.X, p.Y * p2.Y}\n}\n\n// Divide divides the first point by the second.\nfunc (p Point) Divide(p2 Point) Point {\n\treturn Point{p.X / p2.X, p.Y / p2.Y}\n}\n\n// MultiplyScalar multiplies the given point by the scalar.\nfunc (p Point) MultiplyScalar(s int16) Point {\n\treturn Point{p.X * s, p.Y * s}\n}\n\n// DivideScalar divides the given point by the scalar.\nfunc (p Point) DivideScalar(s int16) Point {\n\treturn Point{p.X / s, p.Y / s}\n}\n\n// Within checks if the point is within the specified bounding box.\nfunc (p Point) Within(nw, se Point) bool {\n\treturn Rect{Min: nw, Max: se}.Contains(p)\n}\n\n// WithinRect checks if the point is within the specified bounding box.\nfunc (p Point) WithinRect(box Rect) bool {\n\treturn box.Contains(p)\n}\n\n// WithinSize checks if the point is within the specified bounding box\n// which starts at 0,0 until the width/height provided.\nfunc (p Point) WithinSize(size Point) bool {\n\treturn p.X >= 0 && p.Y >= 0 && p.X < size.X && p.Y < size.Y\n}\n\n// Move moves a point by one in the specified direction.\nfunc (p Point) Move(direction Direction) Point {\n\treturn p.MoveBy(direction, 1)\n}\n\n// MoveBy moves a point by n in the specified direction.\nfunc (p Point) MoveBy(direction Direction, n int16) Point {\n\tswitch direction {\n\tcase North:\n\t\treturn Point{p.X, p.Y - n}\n\tcase NorthEast:\n\t\treturn Point{p.X + n, p.Y - n}\n\tcase East:\n\t\treturn Point{p.X + n, p.Y}\n\tcase SouthEast:\n\t\treturn Point{p.X + n, p.Y + n}\n\tcase South:\n\t\treturn Point{p.X, p.Y + n}\n\tcase SouthWest:\n\t\treturn Point{p.X - n, p.Y + n}\n\tcase West:\n\t\treturn Point{p.X - n, p.Y}\n\tcase NorthWest:\n\t\treturn Point{p.X - n, p.Y - n}\n\tdefault:\n\t\treturn p\n\t}\n}\n\n// DistanceTo calculates manhattan distance to the other point\nfunc (p Point) DistanceTo(other Point) uint32 {\n\treturn abs(int32(p.X)-int32(other.X)) + abs(int32(p.Y)-int32(other.Y))\n}\n\n// Angle calculates the angle between two points\nfunc (p Point) Angle(other Point) Direction {\n\tdx := float64(other.X - p.X)\n\tdy := float64(other.Y - p.Y)\n\n\t// Calculate the angle in radians\n\tangle := math.Atan2(dy, dx)\n\talpha := angle + math.Pi/2\n\tif alpha < 0 {\n\t\talpha += 2 * math.Pi\n\t}\n\n\t// Map to 8 directions (0-7)\n\treturn Direction(math.Round(alpha/(math.Pi/4))) % 8\n}\n\nfunc abs(n int32) uint32 {\n\tif n < 0 {\n\t\treturn uint32(-n)\n\t}\n\treturn uint32(n)\n}\n\n// -----------------------------------------------------------------------------\n\n// Rect represents a rectangle\ntype Rect struct {\n\tMin Point // Top left point of the rectangle\n\tMax Point // Bottom right point of the rectangle\n}\n\n// NewRect creates a new rectangle\n// left,top,right,bottom correspond to x1,y1,x2,y2\nfunc NewRect(left, top, right, bottom int16) Rect {\n\treturn Rect{Min: At(left, top), Max: At(right, bottom)}\n}\n\n// Contains returns whether a point is within the rectangle or not.\nfunc (a Rect) Contains(p Point) bool {\n\treturn a.Min.X <= p.X && p.X < a.Max.X && a.Min.Y <= p.Y && p.Y < a.Max.Y\n}\n\n// Intersects returns whether a rectangle intersects with another rectangle or not.\nfunc (a Rect) Intersects(b Rect) bool {\n\treturn b.Min.X < a.Max.X && a.Min.X < b.Max.X && b.Min.Y < a.Max.Y && a.Min.Y < b.Max.Y\n}\n\n// Size returns the size of the rectangle\nfunc (a *Rect) Size() Point {\n\treturn Point{\n\t\tX: a.Max.X - a.Min.X,\n\t\tY: a.Max.Y - a.Min.Y,\n\t}\n}\n\n// IsZero returns true if the rectangle is zero-value\nfunc (a Rect) IsZero() bool {\n\treturn a.Min.X == a.Max.X && a.Min.Y == a.Max.Y\n}\n\n// Difference calculates up to four non-overlapping regions in a that are not covered by b.\n// If there are fewer than four distinct regions, the remaining Rects will be zero-value.\nfunc (a Rect) Difference(b Rect) (result [4]Rect) {\n\tif b.Contains(a.Min) && b.Contains(a.Max) {\n\t\treturn // Fully covered, return zero-value result\n\t}\n\n\t// Check for non-overlapping cases\n\tif !a.Intersects(b) {\n\t\tresult[0] = a // No overlap, return A as is\n\t\treturn\n\t}\n\n\tleft := min(a.Min.X, b.Min.X)\n\tright := max(a.Max.X, b.Max.X)\n\ttop := min(a.Min.Y, b.Min.Y)\n\tbottom := max(a.Max.Y, b.Max.Y)\n\n\tresult[0].Min = Point{X: left, Y: top}\n\tresult[0].Max = Point{X: right, Y: max(a.Min.Y, b.Min.Y)}\n\n\tresult[1].Min = Point{X: left, Y: min(a.Max.Y, b.Max.Y)}\n\tresult[1].Max = Point{X: right, Y: bottom}\n\n\tresult[2].Min = Point{X: left, Y: top}\n\tresult[2].Max = Point{X: max(a.Min.X, b.Min.X), Y: bottom}\n\n\tresult[3].Min = Point{X: min(a.Max.X, b.Max.X), Y: top}\n\tresult[3].Max = Point{X: right, Y: bottom}\n\n\tif result[0].Size().X == 0 || result[0].Size().Y == 0 {\n\t\tresult[0] = Rect{}\n\t}\n\tif result[1].Size().X == 0 || result[1].Size().Y == 0 {\n\t\tresult[1] = Rect{}\n\t}\n\tif result[2].Size().X == 0 || result[2].Size().Y == 0 {\n\t\tresult[2] = Rect{}\n\t}\n\tif result[3].Size().X == 0 || result[3].Size().Y == 0 {\n\t\tresult[3] = Rect{}\n\t}\n\n\treturn\n}\n\n// Pack returns a packed representation of a rectangle\nfunc (a Rect) pack() uint64 {\n\treturn uint64(a.Min.Integer())<<32 | uint64(a.Max.Integer())\n}\n\n// Unpack returns a rectangle from a packed representation\nfunc unpackRect(v uint64) Rect {\n\treturn Rect{\n\t\tMin: unpackPoint(uint32(v >> 32)),\n\t\tMax: unpackPoint(uint32(v)),\n\t}\n}\n\n// -----------------------------------------------------------------------------\n\n// Diretion represents a direction\ntype Direction byte\n\n// Various directions\nconst (\n\tNorth Direction = iota\n\tNorthEast\n\tEast\n\tSouthEast\n\tSouth\n\tSouthWest\n\tWest\n\tNorthWest\n)\n\n// String returns a string representation of a direction\nfunc (v Direction) String() string {\n\tswitch v {\n\tcase North:\n\t\treturn \"🡱N\"\n\tcase NorthEast:\n\t\treturn \"🡵NE\"\n\tcase East:\n\t\treturn \"🡲E\"\n\tcase SouthEast:\n\t\treturn \"🡶SE\"\n\tcase South:\n\t\treturn \"🡳S\"\n\tcase SouthWest:\n\t\treturn \"🡷SW\"\n\tcase West:\n\t\treturn \"🡰W\"\n\tcase NorthWest:\n\t\treturn \"🡴NW\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// Vector returns a direction vector with a given scale\nfunc (v Direction) Vector(scale int16) Point {\n\treturn Point{}.MoveBy(v, scale)\n}\n\n// angleOf returns the direction from one point to another\nfunc angleOf(from, to Point) Direction {\n\tdx := to.X - from.X\n\tdy := to.Y - from.Y\n\n\tswitch {\n\tcase dx == 0 && dy == -1:\n\t\treturn North\n\tcase dx == 1 && dy == -1:\n\t\treturn NorthEast\n\tcase dx == 1 && dy == 0:\n\t\treturn East\n\tcase dx == 1 && dy == 1:\n\t\treturn SouthEast\n\tcase dx == 0 && dy == 1:\n\t\treturn South\n\tcase dx == -1 && dy == 1:\n\t\treturn SouthWest\n\tcase dx == -1 && dy == 0:\n\t\treturn West\n\tcase dx == -1 && dy == -1:\n\t\treturn NorthWest\n\tdefault:\n\t\treturn Direction(0) // Invalid direction\n\t}\n}\n\n// oppositeDirection returns the opposite of the given direction\nfunc oppositeDirection(dir Direction) Direction {\n\treturn Direction((dir + 4) % 8)\n}\n"
  },
  {
    "path": "point_test.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for details.\n\npackage tile\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n/*\ncpu: 13th Gen Intel(R) Core(TM) i7-13700K\nBenchmarkPoint/within-24         \t1000000000\t         0.09854 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkPoint/within-rect-24    \t1000000000\t         0.09966 ns/op\t       0 B/op\t       0 allocs/op\n*/\nfunc BenchmarkPoint(b *testing.B) {\n\tp := At(10, 20)\n\tb.Run(\"within\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tp.Within(At(0, 0), At(100, 100))\n\t\t}\n\t})\n\n\tb.Run(\"within-rect\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor n := 0; n < b.N; n++ {\n\t\t\tp.WithinRect(NewRect(0, 0, 100, 100))\n\t\t}\n\t})\n}\n\nfunc TestPoint(t *testing.T) {\n\tp := At(10, 20)\n\tp2 := At(2, 2)\n\n\tassert.Equal(t, int16(10), p.X)\n\tassert.Equal(t, int16(20), p.Y)\n\tassert.Equal(t, uint32(0xa0014), p.Integer())\n\tassert.Equal(t, At(-5, 5), unpackPoint(At(-5, 5).Integer()))\n\tassert.Equal(t, \"10,20\", p.String())\n\tassert.True(t, p.Equal(At(10, 20)))\n\tassert.Equal(t, \"20,40\", p.MultiplyScalar(2).String())\n\tassert.Equal(t, \"5,10\", p.DivideScalar(2).String())\n\tassert.Equal(t, \"12,22\", p.Add(p2).String())\n\tassert.Equal(t, \"8,18\", p.Subtract(p2).String())\n\tassert.Equal(t, \"20,40\", p.Multiply(p2).String())\n\tassert.Equal(t, \"5,10\", p.Divide(p2).String())\n\tassert.True(t, p.Within(At(1, 1), At(20, 30)))\n\tassert.True(t, p.WithinRect(NewRect(1, 1, 20, 30)))\n\tassert.False(t, p.WithinSize(At(10, 20)))\n\tassert.True(t, p.WithinSize(At(20, 30)))\n}\n\nfunc TestIntersects(t *testing.T) {\n\tassert.True(t, NewRect(0, 0, 2, 2).Intersects(NewRect(1, 0, 3, 2)))\n\tassert.False(t, NewRect(0, 0, 2, 2).Intersects(NewRect(2, 0, 4, 2)))\n\tassert.False(t, NewRect(10, 10, 12, 12).Intersects(NewRect(9, 12, 11, 14)))\n}\n\nfunc TestDirection(t *testing.T) {\n\tfor i := 0; i < 8; i++ {\n\t\tdir := Direction(i)\n\t\tassert.NotEmpty(t, dir.String())\n\t}\n}\n\nfunc TestDirection_Empty(t *testing.T) {\n\tdir := Direction(9)\n\tassert.Empty(t, dir.String())\n}\n\nfunc TestPointAngle(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfrom     Point\n\t\tto       Point\n\t\texpected Direction\n\t}{\n\t\t// Cardinal directions from origin\n\t\t{\"North\", At(0, 0), At(0, -1), North},\n\t\t{\"East\", At(0, 0), At(1, 0), East},\n\t\t{\"South\", At(0, 0), At(0, 1), South},\n\t\t{\"West\", At(0, 0), At(-1, 0), West},\n\n\t\t// Diagonal directions from origin\n\t\t{\"NorthEast\", At(0, 0), At(1, -1), NorthEast},\n\t\t{\"SouthEast\", At(0, 0), At(1, 1), SouthEast},\n\t\t{\"SouthWest\", At(0, 0), At(-1, 1), SouthWest},\n\t\t{\"NorthWest\", At(0, 0), At(-1, -1), NorthWest},\n\n\t\t// Same point (math.Atan2(0,0) = 0, which maps to East after transformation)\n\t\t{\"Same point\", At(5, 5), At(5, 5), East},\n\n\t\t// Non-origin starting points\n\t\t{\"From 10,10 North\", At(10, 10), At(10, 5), North},\n\t\t{\"From 10,10 East\", At(10, 10), At(15, 10), East},\n\t\t{\"From 10,10 South\", At(10, 10), At(10, 15), South},\n\t\t{\"From 10,10 West\", At(10, 10), At(5, 10), West},\n\t\t{\"From 10,10 NorthEast\", At(10, 10), At(15, 5), NorthEast},\n\t\t{\"From 10,10 SouthEast\", At(10, 10), At(15, 15), SouthEast},\n\t\t{\"From 10,10 SouthWest\", At(10, 10), At(5, 15), SouthWest},\n\t\t{\"From 10,10 NorthWest\", At(10, 10), At(5, 5), NorthWest},\n\n\t\t// Edge cases with larger distances\n\t\t{\"Far North\", At(0, 0), At(0, -100), North},\n\t\t{\"Far East\", At(0, 0), At(100, 0), East},\n\t\t{\"Far South\", At(0, 0), At(0, 100), South},\n\t\t{\"Far West\", At(0, 0), At(-100, 0), West},\n\n\t\t// Angles close to boundaries (testing rounding)\n\t\t{\"Near North boundary\", At(0, 0), At(1, -10), North},\n\t\t{\"Near NorthEast boundary\", At(0, 0), At(10, -10), NorthEast},\n\t\t{\"Near East boundary\", At(0, 0), At(10, -1), East},\n\t\t{\"Near SouthEast boundary\", At(0, 0), At(10, 10), SouthEast},\n\n\t\t// Negative coordinates\n\t\t{\"Negative coords North\", At(-5, -5), At(-5, -10), North},\n\t\t{\"Negative coords East\", At(-5, -5), At(0, -5), East},\n\t\t{\"Negative coords South\", At(-5, -5), At(-5, 0), South},\n\t\t{\"Negative coords West\", At(-5, -5), At(-10, -5), West},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := tc.from.Angle(tc.to)\n\t\t\tassert.Equal(t, tc.expected, result,\n\t\t\t\t\"Point %s to %s should be %s, got %s\",\n\t\t\t\ttc.from.String(), tc.to.String(), tc.expected.String(), result.String())\n\t\t})\n\t}\n}\n\nfunc TestMove(t *testing.T) {\n\ttests := []struct {\n\t\tdir Direction\n\t\tout Point\n\t}{\n\t\t{North, Point{X: 0, Y: -1}},\n\t\t{South, Point{X: 0, Y: 1}},\n\t\t{East, Point{X: 1, Y: 0}},\n\t\t{West, Point{X: -1, Y: 0}},\n\t\t{NorthEast, Point{X: 1, Y: -1}},\n\t\t{NorthWest, Point{X: -1, Y: -1}},\n\t\t{SouthEast, Point{X: 1, Y: 1}},\n\t\t{SouthWest, Point{X: -1, Y: 1}},\n\t\t{Direction(99), Point{}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tassert.Equal(t, tc.out, Point{}.Move(tc.dir), tc.dir.String())\n\t}\n}\n\nfunc TestContains(t *testing.T) {\n\ttests := map[Point]bool{\n\t\t{X: 0, Y: 0}: true,\n\t\t{X: 1, Y: 0}: true,\n\t\t{X: 0, Y: 1}: true,\n\t\t{X: 1, Y: 1}: true,\n\t\t{X: 2, Y: 2}: false,\n\t\t{X: 3, Y: 3}: false,\n\t\t{X: 1, Y: 2}: false,\n\t\t{X: 2, Y: 1}: false,\n\t}\n\n\tfor point, expect := range tests {\n\t\tr := NewRect(0, 0, 2, 2)\n\t\tassert.Equal(t, expect, r.Contains(point), point.String())\n\t}\n}\n\nfunc TestDiff_Right(t *testing.T) {\n\ta := Rect{At(0, 0), At(2, 2)}\n\tb := Rect{At(1, 0), At(3, 2)}\n\n\tdiff := a.Difference(b)\n\tassert.Equal(t, Rect{At(0, 0), At(1, 2)}, diff[2])\n\tassert.Equal(t, Rect{At(2, 0), At(3, 2)}, diff[3])\n}\n\nfunc TestDiff_Left(t *testing.T) {\n\ta := Rect{At(0, 0), At(2, 2)}\n\tb := Rect{At(-1, 0), At(1, 2)}\n\n\tdiff := a.Difference(b)\n\tassert.Equal(t, Rect{At(-1, 0), At(0, 2)}, diff[2])\n\tassert.Equal(t, Rect{At(1, 0), At(2, 2)}, diff[3])\n}\n\nfunc TestDiff_Up(t *testing.T) {\n\ta := Rect{At(0, 0), At(2, 2)}\n\tb := Rect{At(0, -1), At(2, 1)}\n\n\tdiff := a.Difference(b)\n\tassert.Equal(t, Rect{At(0, -1), At(2, 0)}, diff[0])\n\tassert.Equal(t, Rect{At(0, 1), At(2, 2)}, diff[1])\n}\n\nfunc TestDiff_Down(t *testing.T) {\n\ta := Rect{At(0, 0), At(2, 2)}\n\tb := Rect{At(0, 1), At(2, 3)}\n\n\tdiff := a.Difference(b)\n\tassert.Equal(t, Rect{At(0, 0), At(2, 1)}, diff[0])\n\tassert.Equal(t, Rect{At(0, 2), At(2, 3)}, diff[1])\n}\n"
  },
  {
    "path": "store.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE file in the project root for details.\r\n\r\npackage tile\r\n\r\nimport (\r\n\t\"compress/flate\"\r\n\t\"encoding/binary\"\r\n\t\"io\"\r\n\t\"os\"\r\n\t\"unsafe\"\r\n\r\n\t\"github.com/kelindar/iostream\"\r\n)\r\n\r\nconst tileDataSize = int(unsafe.Sizeof([9]Value{}))\r\n\r\n// ---------------------------------- Stream ----------------------------------\r\n\r\n// WriteTo writes the grid to a specific writer.\r\nfunc (m *Grid[T]) WriteTo(dst io.Writer) (n int64, err error) {\r\n\tp1 := At(0, 0)\r\n\tp2 := At(m.Size.X-1, m.Size.Y-1)\r\n\r\n\t// Write the viewport size\r\n\tw := iostream.NewWriter(dst)\r\n\theader := make([]byte, 8)\r\n\tbinary.BigEndian.PutUint16(header[0:2], uint16(p1.X))\r\n\tbinary.BigEndian.PutUint16(header[2:4], uint16(p1.Y))\r\n\tbinary.BigEndian.PutUint16(header[4:6], uint16(p2.X))\r\n\tbinary.BigEndian.PutUint16(header[6:8], uint16(p2.Y))\r\n\tif _, err := w.Write(header); err != nil {\r\n\t\treturn w.Offset(), err\r\n\t}\r\n\r\n\t// Write the grid data\r\n\tm.pagesWithin(p1, p2, func(page *page[T]) {\r\n\t\tbuffer := (*[tileDataSize]byte)(unsafe.Pointer(&page.tiles))[:]\r\n\t\tif _, err := w.Write(buffer); err != nil {\r\n\t\t\treturn\r\n\t\t}\r\n\t})\r\n\treturn w.Offset(), nil\r\n}\r\n\r\n// ReadFrom reads the grid from the reader.\r\nfunc ReadFrom[T comparable](src io.Reader) (grid *Grid[T], err error) {\r\n\tr := iostream.NewReader(src)\r\n\theader := make([]byte, 8)\r\n\tif _, err := io.ReadFull(r, header); err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\r\n\t// Read the size\r\n\tvar view Rect\r\n\tview.Min.X = int16(binary.BigEndian.Uint16(header[0:2]))\r\n\tview.Min.Y = int16(binary.BigEndian.Uint16(header[2:4]))\r\n\tview.Max.X = int16(binary.BigEndian.Uint16(header[4:6]))\r\n\tview.Max.Y = int16(binary.BigEndian.Uint16(header[6:8]))\r\n\r\n\t// Allocate a new grid\r\n\tgrid = NewGridOf[T](view.Max.X+1, view.Max.Y+1)\r\n\tbuf := make([]byte, tileDataSize)\r\n\tgrid.pagesWithin(view.Min, view.Max, func(page *page[T]) {\r\n\t\tif _, err = io.ReadFull(r, buf); err != nil {\r\n\t\t\treturn\r\n\t\t}\r\n\r\n\t\tcopy((*[tileDataSize]byte)(unsafe.Pointer(&page.tiles))[:], buf)\r\n\t})\r\n\treturn\r\n}\r\n\r\n// ---------------------------------- File ----------------------------------\r\n\r\n// WriteFile writes the grid into a flate-compressed binary file.\r\nfunc (m *Grid[T]) WriteFile(filename string) error {\r\n\tfile, err := os.Create(filename)\r\n\tif err != nil {\r\n\t\treturn err\r\n\t}\r\n\r\n\tdefer file.Close()\r\n\twriter, err := flate.NewWriter(file, flate.BestSpeed)\r\n\tif err != nil {\r\n\t\treturn err\r\n\t}\r\n\r\n\t// WriteTo the underlying writer\r\n\tdefer writer.Close()\r\n\t_, err = m.WriteTo(writer)\r\n\treturn err\r\n}\r\n\r\n// Restore restores the grid from the specified file. The grid must\r\n// be written using the corresponding WriteFile() method.\r\nfunc ReadFile[T comparable](filename string) (grid *Grid[T], err error) {\r\n\tif _, err := os.Stat(filename); os.IsNotExist(err) {\r\n\t\treturn nil, os.ErrNotExist\r\n\t}\r\n\r\n\t// Otherwise, attempt to open the file and restore\r\n\tfile, err := os.Open(filename)\r\n\tif err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\r\n\tdefer file.Close()\r\n\treturn ReadFrom[T](flate.NewReader(file))\r\n}\r\n"
  },
  {
    "path": "store_test.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE file in the project root for details.\r\n\r\npackage tile\r\n\r\nimport (\r\n\t\"bytes\"\r\n\t\"compress/flate\"\r\n\t\"os\"\r\n\t\"testing\"\r\n\r\n\t\"github.com/stretchr/testify/assert\"\r\n)\r\n\r\n/*\r\ncpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz\r\nBenchmarkStore/save-8         \t   14455\t     81883 ns/op\t       8 B/op\t       1 allocs/op\r\nBenchmarkStore/read-8         \t    2787\t    399699 ns/op\t  647421 B/op\t       7 allocs/op\r\n*/\r\nfunc BenchmarkStore(b *testing.B) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\r\n\tb.Run(\"save\", func(b *testing.B) {\r\n\t\tout := bytes.NewBuffer(make([]byte, 0, 550000))\r\n\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tout.Reset()\r\n\t\t\tm.WriteTo(out)\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"read\", func(b *testing.B) {\r\n\t\tenc := new(bytes.Buffer)\r\n\t\tm.WriteTo(enc)\r\n\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tReadFrom[string](bytes.NewBuffer(enc.Bytes()))\r\n\t\t}\r\n\t})\r\n\r\n}\r\n\r\nfunc TestSaveLoad(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\r\n\t// Save the map\r\n\tenc := new(bytes.Buffer)\r\n\tn, err := m.WriteTo(enc)\r\n\tassert.NoError(t, err)\r\n\tassert.Equal(t, int64(360008), n)\r\n\r\n\t// Load the map back\r\n\tout, err := ReadFrom[string](enc)\r\n\tassert.NoError(t, err)\r\n\tassert.Equal(t, m.pages, out.pages)\r\n}\r\n\r\nfunc TestSaveLoadFlate(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\r\n\t// Save the map\r\n\toutput := new(bytes.Buffer)\r\n\twriter, err := flate.NewWriter(output, flate.BestSpeed)\r\n\tassert.NoError(t, err)\r\n\r\n\tn, err := m.WriteTo(writer)\r\n\tassert.NoError(t, writer.Close())\r\n\tassert.NoError(t, err)\r\n\tassert.Equal(t, int64(360008), n)\r\n\tassert.Equal(t, int(16533), output.Len())\r\n\r\n\t// Load the map back\r\n\treader := flate.NewReader(output)\r\n\tout, err := ReadFrom[string](reader)\r\n\tassert.NoError(t, err)\r\n\tassert.Equal(t, m.pages, out.pages)\r\n}\r\n\r\nfunc TestSaveLoadFile(t *testing.T) {\r\n\ttemp, err := os.CreateTemp(\"\", \"*\")\r\n\tassert.NoError(t, err)\r\n\tdefer os.Remove(temp.Name())\r\n\r\n\t// Write a test map into temp file\r\n\tm := mapFrom(\"300x300.png\")\r\n\tassert.NoError(t, m.WriteFile(temp.Name()))\r\n\r\n\tfi, _ := temp.Stat()\r\n\tassert.Equal(t, int64(16533), fi.Size())\r\n\r\n\t// Read the map back\r\n\tout, err := ReadFile[string](temp.Name())\r\n\tassert.NoError(t, err)\r\n\tassert.Equal(t, m.pages, out.pages)\r\n}\r\n"
  },
  {
    "path": "view.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for details.\n\npackage tile\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\n// Observer represents a tile update Observer.\ntype Observer[T comparable] interface {\n\tViewport() Rect\n\tResize(Rect, func(Point, Tile[T]))\n\tonUpdate(*Update[T])\n}\n\n// ValueAt represents a tile and its value.\ntype ValueAt struct {\n\tPoint // The point of the tile\n\tValue // The value of the tile\n}\n\n// Update represents a tile update notification.\ntype Update[T comparable] struct {\n\tOld ValueAt // Old tile + value\n\tNew ValueAt // New tile + value\n\tAdd T       // An object was added to the tile\n\tDel T       // An object was removed from the tile\n}\n\nvar _ Observer[string] = (*View[string, string])(nil)\n\n// View represents a view which can monitor a collection of tiles. Type parameters\n// S and T are the state and tile types respectively.\ntype View[S any, T comparable] struct {\n\tGrid  *Grid[T]       // The associated map\n\tInbox chan Update[T] // The update inbox for the view\n\tState S              // The state of the view\n\trect  atomic.Uint64  // The view box\n}\n\n// NewView creates a new view for a map with a given state. State can be anything\n// that is passed to the view and can be used to store additional information.\nfunc NewView[S any, T comparable](m *Grid[T], state S) *View[S, T] {\n\tv := &View[S, T]{\n\t\tGrid:  m,\n\t\tInbox: make(chan Update[T], 32),\n\t\tState: state,\n\t}\n\tv.rect.Store(NewRect(-1, -1, -1, -1).pack())\n\treturn v\n}\n\n// Viewport returns the current viewport of the view.\nfunc (v *View[S, T]) Viewport() Rect {\n\treturn unpackRect(v.rect.Load())\n}\n\n// Resize resizes the viewport and notifies the observers of the changes.\nfunc (v *View[S, T]) Resize(view Rect, fn func(Point, Tile[T])) {\n\tgrid := v.Grid\n\tprev := unpackRect(v.rect.Swap(view.pack()))\n\n\tfor _, diff := range view.Difference(prev) {\n\t\tif diff.IsZero() {\n\t\t\tcontinue // Skip zero-value rectangles\n\t\t}\n\n\t\tgrid.pagesWithin(diff.Min, diff.Max, func(page *page[T]) {\n\t\t\tr := page.Bounds()\n\t\t\tswitch {\n\n\t\t\t// Page is now in view\n\t\t\tcase view.Intersects(r) && !prev.Intersects(r):\n\t\t\t\tif grid.observers.Subscribe(page.point, v) {\n\t\t\t\t\tpage.SetObserved(true) // Mark the page as being observed\n\t\t\t\t}\n\n\t\t\t// Page is no longer in view\n\t\t\tcase !view.Intersects(r) && prev.Intersects(r):\n\t\t\t\tif grid.observers.Unsubscribe(page.point, v) {\n\t\t\t\t\tpage.SetObserved(false) // Mark the page as not being observed\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Callback for each new tile in the view\n\t\t\tif fn != nil {\n\t\t\t\tpage.Each(v.Grid, func(p Point, tile Tile[T]) {\n\t\t\t\t\tif view.Contains(p) && !prev.Contains(p) {\n\t\t\t\t\t\tfn(p, tile)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\n// MoveTo moves the viewport towards a particular direction.\nfunc (v *View[S, T]) MoveTo(angle Direction, distance int16, fn func(Point, Tile[T])) {\n\tp := angle.Vector(distance)\n\tr := v.Viewport()\n\tv.Resize(Rect{\n\t\tMin: r.Min.Add(p),\n\t\tMax: r.Max.Add(p),\n\t}, fn)\n}\n\n// MoveBy moves the viewport towards a particular direction.\nfunc (v *View[S, T]) MoveBy(x, y int16, fn func(Point, Tile[T])) {\n\tr := v.Viewport()\n\tv.Resize(Rect{\n\t\tMin: r.Min.Add(At(x, y)),\n\t\tMax: r.Max.Add(At(x, y)),\n\t}, fn)\n}\n\n// MoveAt moves the viewport to a specific coordinate.\nfunc (v *View[S, T]) MoveAt(nw Point, fn func(Point, Tile[T])) {\n\tr := v.Viewport()\n\tsize := r.Max.Subtract(r.Min)\n\tv.Resize(Rect{\n\t\tMin: nw,\n\t\tMax: nw.Add(size),\n\t}, fn)\n}\n\n// Each iterates over all of the tiles in the view.\nfunc (v *View[S, T]) Each(fn func(Point, Tile[T])) {\n\tr := v.Viewport()\n\tv.Grid.Within(r.Min, r.Max, fn)\n}\n\n// At returns the tile at a specified position.\nfunc (v *View[S, T]) At(x, y int16) (Tile[T], bool) {\n\treturn v.Grid.At(x, y)\n}\n\n// WriteAt updates the entire tile at a specific coordinate.\nfunc (v *View[S, T]) WriteAt(x, y int16, tile Value) {\n\tv.Grid.WriteAt(x, y, tile)\n}\n\n// MergeAt updates the bits of tile at a specific coordinate. The bits are specified\n// by the mask. The bits that need to be updated should be flipped on in the mask.\nfunc (v *View[S, T]) MergeAt(x, y int16, tile, mask Value) {\n\tv.Grid.MaskAt(x, y, tile, mask)\n}\n\n// Close closes the view and unsubscribes from everything.\nfunc (v *View[S, T]) Close() error {\n\tr := v.Viewport()\n\tv.Grid.pagesWithin(r.Min, r.Max, func(page *page[T]) {\n\t\tif v.Grid.observers.Unsubscribe(page.point, v) {\n\t\t\tpage.SetObserved(false) // Mark the page as not being observed\n\t\t}\n\t})\n\treturn nil\n}\n\n// onUpdate occurs when a tile has updated.\nfunc (v *View[S, T]) onUpdate(ev *Update[T]) {\n\tv.Inbox <- *ev // (copy)\n}\n\n// -----------------------------------------------------------------------------\n\n// Pubsub represents a publish/subscribe layer for observers.\ntype pubsub[T comparable] struct {\n\tm   sync.Map  // Concurrent map of observers\n\ttmp sync.Pool // Temporary observer sets for notifications\n}\n\n// Subscribe registers an event listener on a system\nfunc (p *pubsub[T]) Subscribe(page Point, sub Observer[T]) bool {\n\tif v, ok := p.m.Load(page.Integer()); ok {\n\t\treturn v.(*observers[T]).Subscribe(sub)\n\t}\n\n\t// Slow path\n\tv, _ := p.m.LoadOrStore(page.Integer(), newObservers[T]())\n\treturn v.(*observers[T]).Subscribe(sub)\n}\n\n// Unsubscribe deregisters an event listener from a system\nfunc (p *pubsub[T]) Unsubscribe(page Point, sub Observer[T]) bool {\n\tif v, ok := p.m.Load(page.Integer()); ok {\n\t\treturn v.(*observers[T]).Unsubscribe(sub)\n\t}\n\treturn false\n}\n\n// Notify notifies listeners of an update that happened.\nfunc (p *pubsub[T]) Notify1(ev *Update[T], page Point) {\n\tp.Each1(func(sub Observer[T]) {\n\t\tviewport := sub.Viewport()\n\t\tif viewport.Contains(ev.New.Point) || viewport.Contains(ev.Old.Point) {\n\t\t\tsub.onUpdate(ev)\n\t\t}\n\t}, page)\n}\n\n// Notify notifies listeners of an update that happened.\nfunc (p *pubsub[T]) Notify2(ev *Update[T], pages [2]Point) {\n\tp.Each2(func(sub Observer[T]) {\n\t\tviewport := sub.Viewport()\n\t\tif viewport.Contains(ev.New.Point) || viewport.Contains(ev.Old.Point) {\n\t\t\tsub.onUpdate(ev)\n\t\t}\n\t}, pages)\n}\n\n// Each iterates over each observer in a page\nfunc (p *pubsub[T]) Each1(fn func(sub Observer[T]), page Point) {\n\tif v, ok := p.m.Load(page.Integer()); ok {\n\t\tv.(*observers[T]).Each(func(sub Observer[T]) {\n\t\t\tfn(sub)\n\t\t})\n\t}\n}\n\n// Each2 iterates over each observer in a page\nfunc (p *pubsub[T]) Each2(fn func(sub Observer[T]), pages [2]Point) {\n\ttargets := p.tmp.Get().(map[Observer[T]]struct{})\n\tclear(targets)\n\tdefer p.tmp.Put(targets)\n\n\t// Collect all observers from all pages\n\tfor _, page := range pages {\n\t\tif v, ok := p.m.Load(page.Integer()); ok {\n\t\t\tv.(*observers[T]).Each(func(sub Observer[T]) {\n\t\t\t\ttargets[sub] = struct{}{}\n\t\t\t})\n\t\t}\n\t}\n\n\t// Invoke the callback for each observer, once\n\tfor sub := range targets {\n\t\tfn(sub)\n\t}\n}\n\n// -----------------------------------------------------------------------------\n\n// Observers represents a change notifier which notifies the subscribers when\n// a specific tile is updated.\ntype observers[T comparable] struct {\n\tsync.Mutex\n\tsubs []Observer[T]\n}\n\n// newObservers creates a new instance of an change observer.\nfunc newObservers[T comparable]() *observers[T] {\n\treturn &observers[T]{\n\t\tsubs: make([]Observer[T], 0, 8),\n\t}\n}\n\n// Each iterates over each observer\nfunc (s *observers[T]) Each(fn func(sub Observer[T])) {\n\tif s == nil {\n\t\treturn\n\t}\n\n\ts.Lock()\n\tdefer s.Unlock()\n\tfor _, sub := range s.subs {\n\t\tfn(sub)\n\t}\n}\n\n// Subscribe registers an event listener on a system\nfunc (s *observers[T]) Subscribe(sub Observer[T]) bool {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.subs = append(s.subs, sub)\n\treturn len(s.subs) > 0 // At least one\n}\n\n// Unsubscribe deregisters an event listener from a system\nfunc (s *observers[T]) Unsubscribe(sub Observer[T]) bool {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\tclean := s.subs[:0]\n\tfor _, o := range s.subs {\n\t\tif o != sub {\n\t\t\tclean = append(clean, o)\n\t\t}\n\t}\n\ts.subs = clean\n\treturn len(s.subs) == 0\n}\n"
  },
  {
    "path": "view_test.go",
    "content": "// Copyright (c) Roman Atachiants and contributors. All rights reserved.\r\n// Licensed under the MIT license. See LICENSE file in the project root for details.\r\n\r\npackage tile\r\n\r\nimport (\r\n\t\"testing\"\r\n\t\"unsafe\"\r\n\r\n\t\"github.com/stretchr/testify/assert\"\r\n)\r\n\r\n/*\r\ncpu: 13th Gen Intel(R) Core(TM) i7-13700K\r\nBenchmarkView/write-24         \t 9540012\t       125.0 ns/op\t      48 B/op\t       1 allocs/op\r\nBenchmarkView/move-24          \t   16141\t     74408 ns/op\t       0 B/op\t       0 allocs/op\r\n*/\r\nfunc BenchmarkView(b *testing.B) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(100, 0, 200, 100), nil)\r\n\r\n\tgo func() {\r\n\t\tfor range v.Inbox {\r\n\t\t}\r\n\t}()\r\n\r\n\tb.Run(\"write\", func(b *testing.B) {\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tv.WriteAt(152, 52, Value(0))\r\n\t\t}\r\n\t})\r\n\r\n\tb.Run(\"move\", func(b *testing.B) {\r\n\t\tlocs := []Point{\r\n\t\t\tAt(100, 0),\r\n\t\t\tAt(200, 100),\r\n\t\t}\r\n\r\n\t\tb.ReportAllocs()\r\n\t\tb.ResetTimer()\r\n\t\tfor n := 0; n < b.N; n++ {\r\n\t\t\tv.MoveAt(locs[n%2], nil)\r\n\t\t}\r\n\t})\r\n}\r\n\r\nfunc TestView(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\r\n\t// Create a new view\r\n\tc := counter(0)\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(100, 0, 200, 100), c.count)\r\n\tassert.NotNil(t, v)\r\n\tassert.Equal(t, 10000, int(c))\r\n\r\n\t// Resize to 10x10\r\n\tc = counter(0)\r\n\tv.Resize(NewRect(0, 0, 10, 10), c.count)\r\n\tassert.Equal(t, 100, int(c))\r\n\r\n\t// Move down-right\r\n\tc = counter(0)\r\n\tv.MoveBy(2, 2, c.count)\r\n\tassert.Equal(t, 48, int(c))\r\n\r\n\t// Move at location\r\n\tc = counter(0)\r\n\tv.MoveAt(At(4, 4), c.count)\r\n\tassert.Equal(t, 48, int(c))\r\n\r\n\t// Each\r\n\tc = counter(0)\r\n\tv.Each(c.count)\r\n\tassert.Equal(t, 100, int(c))\r\n\r\n\t// Update a tile in view\r\n\tcursor, _ := v.At(5, 5)\r\n\tbefore := cursor.Value()\r\n\tv.WriteAt(5, 5, Value(55))\r\n\tupdate := <-v.Inbox\r\n\tassert.Equal(t, At(5, 5), update.New.Point)\r\n\tassert.NotEqual(t, before, update.New)\r\n\r\n\t// Merge a tile in view, but with zero mask (won't do anything)\r\n\tcursor, _ = v.At(5, 5)\r\n\tbefore = cursor.Value()\r\n\tv.MergeAt(5, 5, Value(66), Value(0)) // zero mask\r\n\tupdate = <-v.Inbox\r\n\tassert.Equal(t, At(5, 5), update.New.Point)\r\n\tassert.Equal(t, before, update.New.Value)\r\n\r\n\t// Close the view\r\n\tassert.NoError(t, v.Close())\r\n\tv.WriteAt(5, 5, Value(66))\r\n\tassert.Equal(t, 0, len(v.Inbox))\r\n}\r\n\r\nfunc TestUpdates_Simple(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\tc := counter(0)\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(0, 0, 10, 10), c.count)\r\n\r\n\tassert.NotNil(t, v)\r\n\tassert.Equal(t, 100, int(c))\r\n\r\n\t// Update a tile in view\r\n\tcursor, _ := v.At(5, 5)\r\n\tcursor.Write(Value(0xF0))\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xF0),\r\n\t\t},\r\n\t}, <-v.Inbox)\r\n\r\n\t// Add an object to an observed tile\r\n\tcursor.Add(\"A\")\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xF0),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xF0),\r\n\t\t},\r\n\t\tAdd: \"A\",\r\n\t}, <-v.Inbox)\r\n\r\n\t// Delete an object from an observed tile\r\n\tcursor.Del(\"A\")\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xF0),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xF0),\r\n\t\t},\r\n\t\tDel: \"A\",\r\n\t}, <-v.Inbox)\r\n\r\n\t// Mask a tile in view\r\n\tcursor.Mask(0xFF, 0x0F)\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xF0),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xFF),\r\n\t\t},\r\n\t}, <-v.Inbox)\r\n\r\n\t// Merge a tile in view\r\n\tcursor.Merge(func(v Value) Value {\r\n\t\treturn 0xAA\r\n\t})\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xFF),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t\tValue: Value(0xAA),\r\n\t\t},\r\n\t}, <-v.Inbox)\r\n}\r\n\r\nfunc TestMove_Within(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\tc := counter(0)\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(0, 0, 10, 10), c.count)\r\n\r\n\t// Add an object to an observed tile. This should only fire once since\r\n\t// both the old and new states are the observed by the view.\r\n\tcursor, _ := v.At(5, 5)\r\n\tcursor.Move(\"A\", At(6, 6))\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(6, 6),\r\n\t\t},\r\n\t\tDel: \"A\",\r\n\t\tAdd: \"A\",\r\n\t}, <-v.Inbox)\r\n}\r\n\r\nfunc TestMove_Incoming(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\tc := counter(0)\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(0, 0, 10, 10), c.count)\r\n\r\n\t// Add an object to an observed tile from outside the view.\r\n\tcursor, _ := v.At(20, 20)\r\n\tcursor.Move(\"A\", At(5, 5))\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(20, 20),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t},\r\n\t\tDel: \"A\",\r\n\t\tAdd: \"A\",\r\n\t}, <-v.Inbox)\r\n}\r\n\r\nfunc TestMove_Outgoing(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\tc := counter(0)\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(0, 0, 10, 10), c.count)\r\n\r\n\t// Move an object from an observed tile outside of the view.\r\n\tcursor, _ := v.At(5, 5)\r\n\tcursor.Move(\"A\", At(20, 20))\r\n\tassert.Equal(t, Update[string]{\r\n\t\tOld: ValueAt{\r\n\t\t\tPoint: At(5, 5),\r\n\t\t},\r\n\t\tNew: ValueAt{\r\n\t\t\tPoint: At(20, 20),\r\n\t\t},\r\n\t\tDel: \"A\",\r\n\t\tAdd: \"A\",\r\n\t}, <-v.Inbox)\r\n}\r\n\r\nfunc TestView_MoveTo(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\r\n\t// Create a new view\r\n\tc := counter(0)\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(10, 10, 12, 12), c.count)\r\n\r\n\tassert.NotNil(t, v)\r\n\tassert.Equal(t, 4, int(c))\r\n\tassert.Equal(t, 9, countObservers(m))\r\n\r\n\tconst distance = 10\r\n\r\n\tassert.Equal(t, 1, countObserversAt(m, 10, 10))\r\n\tfor i := 0; i < distance; i++ {\r\n\t\tv.MoveTo(East, 1, c.count)\r\n\t}\r\n\r\n\tassert.Equal(t, 0, countObserversAt(m, 10, 10))\r\n\tfor i := 0; i < distance; i++ {\r\n\t\tv.MoveTo(South, 1, c.count)\r\n\t}\r\n\r\n\tassert.Equal(t, 0, countObserversAt(m, 10, 10))\r\n\tfor i := 0; i < distance; i++ {\r\n\t\tv.MoveTo(West, 1, c.count)\r\n\t}\r\n\r\n\tassert.Equal(t, 0, countObserversAt(m, 10, 10))\r\n\tfor i := 0; i < distance; i++ {\r\n\t\tv.MoveTo(North, 1, c.count)\r\n\t}\r\n\r\n\t// Start should have the observer attached\r\n\tassert.Equal(t, 1, countObserversAt(m, 10, 10))\r\n\tassert.Equal(t, 0, countObserversAt(m, 100, 100))\r\n\r\n\t// Count the number of observers, should be the same as before\r\n\tassert.Equal(t, 9, countObservers(m))\r\n\tassert.NoError(t, v.Close())\r\n}\r\n\r\nfunc TestView_Updates(t *testing.T) {\r\n\tm := mapFrom(\"300x300.png\")\r\n\tv := NewView(m, \"view 1\")\r\n\tv.Resize(NewRect(10, 10, 15, 15), nil)\r\n\r\n\tmove := func(x1, y1, x2, y2 int16) {\r\n\t\tat, _ := m.At(x1, y1)\r\n\t\tat.Move(\"A\", At(x2, y2))\r\n\r\n\t\tassert.Equal(t, Update[string]{\r\n\t\t\tOld: ValueAt{Point: At(x1, y1)},\r\n\t\t\tNew: ValueAt{Point: At(x2, y2)},\r\n\t\t\tDel: \"A\", Add: \"A\",\r\n\t\t}, <-v.Inbox)\r\n\t}\r\n\r\n\tmove(9, 12, 10, 12)  // Enter from left edge\r\n\tmove(10, 12, 9, 12)  // Exit to left edge\r\n\tmove(15, 12, 14, 12) // Enter from right edge\r\n\tmove(14, 12, 15, 12) // Exit to right edge\r\n\tmove(12, 9, 12, 10)  // Enter from top edge\r\n\tmove(12, 10, 12, 9)  // Exit to top edge\r\n\tmove(12, 15, 12, 14) // Enter from bottom edge\r\n\tmove(12, 14, 12, 15) // Exit to bottom edge\r\n\tmove(9, 9, 10, 10)   // Enter from top-left diagonal\r\n\tmove(10, 10, 9, 9)   // Exit to top-left diagonal\r\n\tmove(15, 9, 14, 10)  // Enter from top-right diagonal\r\n\tmove(14, 10, 15, 9)  // Exit to top-right diagonal\r\n\tmove(9, 15, 10, 14)  // Enter from bottom-left diagonal\r\n\tmove(10, 14, 9, 15)  // Exit to bottom-left diagonal\r\n\tmove(15, 15, 14, 14) // Enter from bottom-right diagonal\r\n\tmove(14, 14, 15, 15) // Exit to bottom-right diagonal\r\n\r\n\tassert.NoError(t, v.Close())\r\n}\r\n\r\nfunc TestSizeUpdate(t *testing.T) {\r\n\tassert.Equal(t, 24, int(unsafe.Sizeof(Update[uint32]{})))\r\n}\r\n\r\n// ---------------------------------- Mocks ----------------------------------\r\n\r\nfunc countObserversAt(m *Grid[string], x, y int16) (count int) {\r\n\tstart, _ := m.At(x, y)\r\n\tstart.Observers(func(view Observer[string]) {\r\n\t\tcount++\r\n\t})\r\n\treturn count\r\n}\r\n\r\nfunc countObservers(m *Grid[string]) int {\r\n\tvar observers int\r\n\tm.Each(func(p Point, t Tile[string]) {\r\n\t\tif t.IsObserved() {\r\n\t\t\tobservers++\r\n\t\t}\r\n\t})\r\n\treturn observers\r\n}\r\n\r\ntype fakeView[T comparable] func(*Update[T])\r\n\r\nfunc (f fakeView[T]) Viewport() Rect {\r\n\treturn Rect{}\r\n}\r\n\r\nfunc (f fakeView[T]) Resize(r Rect, fn func(Point, Tile[T])) {\r\n\t// Do nothing\r\n}\r\n\r\nfunc (f fakeView[T]) onUpdate(e *Update[T]) {\r\n\tf(e)\r\n}\r\n\r\ntype counter int\r\n\r\nfunc (c *counter) count(p Point, tile Tile[string]) {\r\n\t*c++\r\n}\r\n"
  }
]