Skip to content

Commit e674772

Browse files
authored
add a general ObjectIndexableArray type (#9)
* add ObjectIndexableArray * add get_objects method * rename get_object_types to get_objects_type * add tests for get_objects_type, get_num_objects, get_height, and get_width * update README * fix tests
1 parent c080d36 commit e674772

7 files changed

Lines changed: 84 additions & 54 deletions

File tree

README.md

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,33 @@
22

33
`TileMaps` is a package that makes it simple to create 2D tile maps in Julia. It is designed to be lightweight and fast, and have minimal dependencies.
44

5-
**Note:** This package does not export any symbols. The examples below that demonstrate the use of this package assume that it has been loaded via `import TileMaps as TM`.
5+
**Note:** This package does not export any names. The examples below that demonstrate the use of this package assume that it has been loaded via `import TileMaps as TM`.
66

77
**Acknowledgements:** Big thanks to [Jun Tian](https://github.com/findmyway) (@findmyway) for initially introducing the core ideas of this package in [`GridWorlds`](https://github.com/JuliaReinforcementLearning/GridWorlds.jl).
88

99
### Index
1010

11-
1. [TileMap](#tilemap)
11+
1. [ObjectIndexableArray](#objectindexablearray)
1212
1. [Objects](#objects)
13+
1. [TileMap](#tilemap)
1314
1. [Constructing a TileMap](#constructing-a-tilemap)
1415
1. [Indexing a TileMap](#indexing-a-tilemap)
1516
1. [Visualizing a TileMap](#visualizing-a-tilemap)
1617

17-
### TileMap
18+
19+
### `ObjectIndexableArray`
1820

1921
```
20-
struct TileMap{O} <: AbstractArray{Bool, 3}
21-
grid::BitArray{3}
22-
objects::O
22+
struct ObjectIndexableArray{T, N, A, O} <: AbstractArray{T, N}
23+
array::A
2324
end
2425
```
2526

26-
We'll refer to an instance of `TileMap` as `tile_map`. A `tile_map` contains a field called `grid` of type `BitArray{3}` and size `(num_objects, height, width)`, which efficiently stores the objects present in the `tile_map`. Each tile can contain multiple objects, which is captured by a multi-hot encoding along the first dimension (`num_objects` dimension) of the `grid`.
27+
We'll refer to an instance of `ObjectIndexableArray` as `object_indexable_array`. An `object_indexable_array` simply wraps an `array` and allows us to index its first dimension using a [singleton](https://docs.julialang.org/en/v1/manual/types/#man-singleton-types) object or an array of singleton objects (in addition to all the other ways of indexing `array`).
2728

2829
### Objects
2930

30-
Objects are [singletons](https://docs.julialang.org/en/v1/manual/types/#man-singleton-types) (structs with no fields). Here is how objects are created inside this package:
31+
Object types are [singletons](https://docs.julialang.org/en/v1/manual/types/#man-singleton-types) (structs with no fields). Here are the example objects that are provided in this package:
3132

3233
```
3334
abstract type AbstractObject end
@@ -50,27 +51,37 @@ julia> struct MyObject <: TM.AbstractObject end
5051
julia>
5152
```
5253

54+
### TileMap
55+
56+
```
57+
const TileMap{O} = ObjectIndexableArray{Bool, 3, BitArray{3}, O}
58+
```
59+
60+
We'll refer to an instance of `TileMap` as `tile_map`. A `tile_map` wraps an `array` of size `(num_objects, height, width)`, which encodes information about the presence or absence of objects across the tiles. Each tile can contain multiple objects, which is captured by a multi-hot encoding along the first dimension (`num_objects` dimension) of the `array`.
61+
5362
### Constructing a `TileMap`
5463

55-
You can instantiate a `TileMap` using a constructor that is provided by this package. The following creates an empty tile map using a tuple of objects along with the desired height and width:
64+
You can instantiate a `TileMap` using the following constructor that are provided by this package:
5665

57-
```
58-
julia> tile_map = TM.TileMap((TM.EXAMPLE_OBJECT_1, TM.EXAMPLE_OBJECT_2, TM.EXAMPLE_OBJECT_3), 8, 16);
66+
1. Create an empty `tile_map` using a tuple of objects and the desired height (8) and width(16):
5967

60-
julia>
61-
```
68+
```
69+
julia> tile_map = TM.TileMap((TM.EXAMPLE_OBJECT_1, TM.EXAMPLE_OBJECT_2, TM.EXAMPLE_OBJECT_3), 8, 16);
70+
71+
julia>
72+
```
6273
63-
Or you could directly use the default constructor as well:
74+
1. Create a `tile_map` using a tuple of objects and an existing `array`:
6475
6576
```
66-
julia> tile_map = TM.TileMap(rand(Bool, 3, 8, 16) |> BitArray, (TM.EXAMPLE_OBJECT_1, TM.EXAMPLE_OBJECT_2, TM.EXAMPLE_OBJECT_3));
77+
julia> tile_map = TM.TileMap((TM.EXAMPLE_OBJECT_1, TM.EXAMPLE_OBJECT_2, TM.EXAMPLE_OBJECT_3), rand(Bool, 3, 8, 16) |> BitArray);
6778

6879
julia>
6980
```
7081
7182
### Indexing a `TileMap`
7283
73-
In addition to the normal ways of indexing an array, you can also use an object or an array of objects to index the first dimension of a `tile_map`. For example, something like this:
84+
Because of `TileMap <: ObjectIndexableArray`, we can index the first dimension of a `tile_map` in a variety of ways. For example, like this:
7485
7586
```
7687
julia> tile_map[TM.EXAMPLE_OBJECT_3, 4, 6]
@@ -92,23 +103,25 @@ julia>
92103
93104
### Visualizing a `TileMap`
94105
95-
Using the [`Crayons`](https://github.com/KristofferC/Crayons.jl) package, each object can be displayed as a colored Unicode character:
106+
Using the [`Crayons`](https://github.com/KristofferC/Crayons.jl) package, each object can be displayed as a colored Unicode character. For example, like this:
96107
97108
<img src="https://github.com/Sid-Bhatia-0/TileMaps.jl/blob/master/assets/example_object_1.png">
98109
99110
When you create your custom object like this, for example,
100111
101112
<img src="https://github.com/Sid-Bhatia-0/TileMaps.jl/blob/master/assets/my_object.png">
102113
103-
you may also want to implement the following methods: `get_char(::MyObject)` (especially this one), `get_foreground_color(::MyObject)`, and `get_backround_color(::MyObject)`. If you don't do so, it will be displayed based on the following default methods defined in this package:
114+
you may also want to implement the following methods: `get_char(::MyObject)` (especially this one), `get_foreground_color(::MyObject)`, and `get_backround_color(::MyObject)`. If you don't do so, `MY_OBJECT` will be displayed based on the following default methods as defined in this package:
104115
105116
```
106117
get_char(object::Any) = '?'
107118
get_foreground_color(object::Any) = :white
108119
get_backround_color(object::Any) = :nothing
109120
```
110121
111-
A `tile_map` is displayed as a 2D grid of colored Unicode characters, with one character displayed per tile. Only the first object present at a tile (along the first dimension (`num_objects` dimension) of the `grid`) is displayed for that tile, even though there may be multiple objects present at that tile. If there are no objects present at a tile, then the `` character is displayed for that tile (with white color). This behaviour can be customized by overriding the following methods:
122+
A `tile_map` is displayed as a 2D grid of colored Unicode characters, with one character displayed per tile. Only the first object present (along the first dimension (`num_objects` dimension) of the `array`) at a tile is displayed for that tile, even though there may be multiple objects present at that tile.
123+
124+
If there are no objects present at a tile, then the `⋅` character is displayed for that tile (with white color and no background). This behaviour can be customized by overriding the following methods:
112125
113126
```
114127
get_char(::Nothing) = '⋅'
@@ -118,10 +131,10 @@ get_background_color(::Nothing) = :nothing
118131
119132
<img src="https://github.com/Sid-Bhatia-0/TileMaps.jl/blob/master/assets/tile_map.png">
120133
121-
Note that the `get_char`, `get_foreground_color`, and `get_background_color` methods have purposefully not been defined for the `TM.ExampleObject3` type in order to demonstrate the fallback to the default character and colors, which is why `TM.EXAMPLE_OBJECT_3` is displayed using a white colored `?` with no background.
134+
Note that the `get_char`, `get_foreground_color`, and `get_background_color` methods have purposefully not been explictly defined for the `TM.ExampleObject3` type in order to demonstrate the fallback to the default character and colors, which is why `TM.EXAMPLE_OBJECT_3` is displayed using a white colored `?` with no background.
122135
123136
We can also inspect each kind of object in the `tile_map` separately using the `show_layers` method. This is very handy for debugging:
124137
125138
<img src="https://github.com/Sid-Bhatia-0/TileMaps.jl/blob/master/assets/show_layers.png">
126139
127-
Here we have used only a limited number of features (foreground and background colors) from the [`Crayons`](https://github.com/KristofferC/Crayons.jl) package for showing an example of how one may want to display a `tile_map`. It has several other features that you can play with to suit your needs.
140+
Here we have utilized only a limited number of features from the `Crayons` package in order to show an an example of how one may want to display a `tile_map`. `Crayons` has other features that you can play with to suit your needs.

src/TileMaps.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module TileMaps
33
import Crayons
44

55
include("objects.jl")
6+
include("object_indexable_array.jl")
67
include("tile_map.jl")
78
include("visualization.jl")
89

src/object_indexable_array.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
struct ObjectIndexableArray{T, N, A, O} <: AbstractArray{T, N}
2+
array::A
3+
end
4+
5+
get_objects_type(::ObjectIndexableArray{T, N, A, O}) where {T, N, A, O} = O
6+
get_objects(object_indexable_array::ObjectIndexableArray) = Tuple(object_type() for object_type in get_objects_type(object_indexable_array).parameters)
7+
8+
Base.size(object_indexable_array::ObjectIndexableArray, args...; kwargs...) = Base.size(object_indexable_array.array, args..., kwargs...)
9+
10+
# regular indexing (indexing without using objects)
11+
Base.getindex(object_indexable_array::ObjectIndexableArray, args...; kwargs...) = Base.getindex(object_indexable_array.array, args..., kwargs...)
12+
Base.setindex!(object_indexable_array::ObjectIndexableArray, args...; kwargs...) = Base.setindex!(object_indexable_array.array, args..., kwargs...)
13+
14+
# indexing using a single object as index
15+
@generated function Base.to_index(object_indexable_array::ObjectIndexableArray{T, N, A, O}, object::X) where {T, N, A, O, X <: AbstractObject}
16+
i = findfirst(X .=== O.parameters)
17+
isnothing(i) && error("Object $object is not present in $object_indexable_array")
18+
return :($i)
19+
end
20+
21+
Base.getindex(object_indexable_array::ObjectIndexableArray, object::AbstractObject, args...; kwargs...) = getindex(object_indexable_array.array, Base.to_index(object_indexable_array, object), args..., kwargs...)
22+
Base.setindex!(object_indexable_array::ObjectIndexableArray, value::Bool, object::AbstractObject, args...; kwargs...) = setindex!(object_indexable_array.array, value, Base.to_index(object_indexable_array, object), args..., kwargs...)
23+
24+
# indexing using more than one object
25+
Base.to_index(object_indexable_array::ObjectIndexableArray, objects::AbstractArray{<:AbstractObject}) = map(object -> Base.to_index(object_indexable_array, object), objects)
26+
Base.getindex(object_indexable_array::ObjectIndexableArray, objects::AbstractArray{<:AbstractObject}, args...; kwargs...) = getindex(object_indexable_array.array, map(object -> Base.to_index(object_indexable_array, object), objects), args..., kwargs...)

src/object_occupancy_array.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const ObjectOccupancyArray{O, N} = ObjectIndexableArray{O, BitArray{N}, Bool, N}
2+
3+
function ObjectOccupancyArray(objects::Tuple{Vararg{AbstractObject}}, dims::Integer...)
4+
grid = falses(length(objects), dims...)
5+
return ObjectOccupancyArray{typeof(objects), length(dims) + 1}(grid)
6+
end

src/tile_map.jl

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,13 @@
11
"""
2-
TileMap{O} <: AbstractArray{Bool, 3}
32
The first dimension uses multi-hot encoding to encode objects in a tile.
43
The second and third dimensions correspond to the height and width of the tile map respectively.
54
"""
6-
struct TileMap{O} <: AbstractArray{Bool, 3}
7-
grid::BitArray{3}
8-
objects::O
9-
end
5+
const TileMap{O} = ObjectIndexableArray{Bool, 3, BitArray{3}, O}
106

11-
function TileMap(objects::Tuple{Vararg{AbstractObject}}, height::Integer, width::Integer)
12-
grid = falses(length(objects), height, width)
13-
return TileMap(grid, objects)
14-
end
7+
TileMap(objects::Tuple{Vararg{AbstractObject}}, grid::BitArray{3}) = TileMap{typeof(objects)}(grid)
8+
9+
TileMap(objects::Tuple{Vararg{AbstractObject}}, height::Integer, width::Integer) = TileMap(objects, falses(length(objects), height, width))
1510

16-
Base.size(tile_map::TileMap, args...; kwargs...) = Base.size(tile_map.grid, args..., kwargs...)
1711
get_num_objects(tile_map::TileMap) = size(tile_map, 1)
1812
get_height(tile_map::TileMap) = size(tile_map, 2)
1913
get_width(tile_map::TileMap) = size(tile_map, 3)
20-
21-
# regular indexing (indexing without using objects)
22-
Base.getindex(tile_map::TileMap, args...; kwargs...) = Base.getindex(tile_map.grid, args..., kwargs...)
23-
Base.setindex!(tile_map::TileMap, args...; kwargs...) = Base.setindex!(tile_map.grid, args..., kwargs...)
24-
25-
# indexing using one object
26-
@generated function Base.to_index(tile_map::TileMap{O}, object::X) where {X <: AbstractObject, O}
27-
i = findfirst(X .=== O.parameters)
28-
isnothing(i) && error("Object $object is not present in $tile_map")
29-
return :($i)
30-
end
31-
32-
Base.getindex(tile_map::TileMap, object::AbstractObject, args...; kwargs...) = getindex(tile_map.grid, Base.to_index(tile_map, object), args..., kwargs...)
33-
Base.setindex!(tile_map::TileMap, value::Bool, object::AbstractObject, args...; kwargs...) = setindex!(tile_map.grid, value, Base.to_index(tile_map, object), args..., kwargs...)
34-
35-
# indexing using more than one object
36-
Base.to_index(tile_map::TileMap, objects::AbstractArray{<:AbstractObject}) = map(object -> Base.to_index(tile_map, object), objects)
37-
Base.getindex(tile_map::TileMap, objects::AbstractArray{<:AbstractObject}, args...; kwargs...) = getindex(tile_map.grid, map(object -> Base.to_index(tile_map, object), objects), args..., kwargs...)

src/visualization.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ function Base.show(io::IO, ::MIME"text/plain", object::AbstractObject)
2929
return nothing
3030
end
3131

32-
function get_first_object(tile_map::TileMap, height::Integer, width::Integer)
32+
function get_first_object(tile_map::TileMap{O}, height::Integer, width::Integer) where {O}
3333
idx = findfirst(tile_map[:, height, width])
3434
if isnothing(idx)
3535
return nothing
3636
else
37-
return tile_map.objects[idx]
37+
return O.parameters[idx]()
3838
end
3939
end
4040

@@ -59,8 +59,9 @@ function Base.show(io::IO, ::MIME"text/plain", tile_map::TileMap)
5959
return nothing
6060
end
6161

62-
function show_layers(io::IO, ::MIME"text/plain", tile_map::TileMap)
63-
for (layer, object) in enumerate(tile_map.objects)
62+
function show_layers(io::IO, ::MIME"text/plain", tile_map::TileMap{O}) where {O}
63+
for (layer, object_type) in enumerate(O.parameters)
64+
object = object_type()
6465
println("layer = $layer, object = $object")
6566
for i in 1:get_height(tile_map)
6667
for j in 1:get_width(tile_map)

test/runtests.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ Test.@testset "TileMaps.jl" begin
2525
grid[2, :, :] .= layer_2
2626
grid[3, :, :] .= layer_3
2727

28-
tile_map = TM.TileMap(grid, objects)
28+
tile_map = TM.TileMap(objects, grid)
29+
30+
Test.@test TM.get_objects_type(tile_map) == typeof(objects)
31+
Test.@test TM.get_objects(tile_map) == objects
32+
33+
Test.@test TM.get_num_objects(tile_map) == 3
34+
Test.@test TM.get_height(tile_map) == 4
35+
Test.@test TM.get_width(tile_map) == 5
2936

3037
# regular indexing (indexing without using objects)
3138
Test.@test tile_map[1, 2, 3] == false

0 commit comments

Comments
 (0)