Skip to content

Commit

Permalink
update README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
findmyway committed Jul 16, 2020
1 parent 671e4a5 commit c241130
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 1 deletion.
7 changes: 7 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ uuid = "34dccd9f-48d6-4445-aa0f-8c2e373b5429"
authors = ["Jun Tian <[email protected]> and contributors"]
version = "0.1.0"

[deps]
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[compat]
julia = "1"
Colors = "0.12"
Makie = "0.11"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,83 @@
[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaReinforcementLearning.github.io/SnakeGames.jl/stable)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaReinforcementLearning.github.io/SnakeGames.jl/dev)
[![Build Status](https://travis-ci.com/JuliaReinforcementLearning/SnakeGames.jl.svg?branch=master)](https://travis-ci.com/JuliaReinforcementLearning/SnakeGames.jl)

This package provides some basic variants of the [snake game](https://en.wikipedia.org/wiki/Snake_(video_game)).

## Basic Usage

```julia
pkg> add SnakeGames

julia> using SnakeGames

julia> play()
```

Single snake and single food. The snake can move through the boundary.

![](img/single_snake_single_food_no_wall.gif)

<hr>

```julia
game = SnakeGame(;walls=[
CartesianIndex.(1, 1:8)...,
CartesianIndex.(8, 1:8)...,
CartesianIndex.(1:8, 1)...,
CartesianIndex.(1:8, 8)...])

play(game)
```

Add boundaries to the game. The game stop when the snake hits the wall.

![](img/single_snake_single_food_with_walls.gif)

<hr>

```julia
game = SnakeGame(;n_snakes=2)

play(game)
```

2 snakes and 1 food. Game stop when two snake eat the same food.

![](img/two_snakes_single_food_no_walls.gif)

<hr>

A known bug is that, two snakes of length 1 can move across each other.

![](img/known_bug.gif)

<hr>

```julia
game = SnakeGame(;n_snakes=3, n_foods=5)

play(game)
```

3 snakes and 5 foods. Game stop when one snake hits another.

![](img/3_snakes_5_foods_no_walls.gif)

<hr>

In fact, we can have many snakes and foods.

![](img/multiple_snakes_multiple_rewards.png)

And even in the 3D mode. (TODO: add a picture.)

## Inner Representation

By default, a vector of `2*n_snakes+2` bits is used to represent the current state of each grid.

- The first `n_snakes` bits are used to mark which snakes' head occupy the grid.
- The following up `n_snakes` bits are used to mark which snakes' body occupy the grid.
- The last two bits are used to mark whether this grid is occupied by wall/food or not.

You can access it via `game.board` and use it in your own algorithms.
Binary file added img/3_snakes_5_foods_no_walls.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/known_bug.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/multiple_snakes_multiple_rewards.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/single_snake_single_food_no_wall.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/single_snake_single_food_with_walls.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/two_snakes_single_food_no_walls.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/SnakeGames.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module SnakeGames

# Write your package code here.
include("snake_game.jl")
include("interaction.jl")

end
94 changes: 94 additions & 0 deletions src/interaction.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
export init_screen, play

using Makie
using Colors

function init_screen(game::Observable{<:SnakeGame{2}}; resolution=(1000,1000))
SNAKE_COLORS = range(HSV(60,1,1), stop=HSV(300,1,1), length=length(game[].snakes)+1)
scene = Scene(resolution = resolution, raw = true, camera = campixel!)

area = scene.px_area
poly!(scene, area)

grid_size = @lift((widths($area)[1] / size($game)[1], widths($area)[2] / size($game)[2]))

walls = get_walls(game[])
if length(walls) > 0
wall_boxes = @lift([FRect2D((w.I .- (1,1)) .* $grid_size , $grid_size) for w in walls])
poly!(scene, wall_boxes, color=:gray)
end

for i in 1:length(game[].snakes)
snake_boxes = @lift([FRect2D((p.I .- (1,1)) .* $grid_size , $grid_size) for p in $game.snakes[i]])
poly!(scene, snake_boxes, color=SNAKE_COLORS[i], strokewidth = 5, strokecolor = :black)

snake_head_box = @lift(FRect2D(($game.snakes[i][1].I .- (1,1)) .* $grid_size , $grid_size))
poly!(scene, snake_head_box, color=:black)
snake_head = @lift((($game.snakes[i][1].I .- 0.5) .* $grid_size))
scatter!(scene, snake_head, marker='', color=SNAKE_COLORS[i], markersize=@lift(minimum($grid_size)))
end

food_position = @lift([(f.I .- (0.5, 0.5)) .* $grid_size for f in $game.foods])
scatter!(scene, food_position, color=:red, marker='', markersize=@lift(minimum($grid_size)))

display(scene)
scene
end

play() = play(SnakeGame())

function play(game::SnakeGame{2};f_name="test.gif",framerate = 2)
@assert length(game.snakes) <= 3 "At most three players are supported in interactive mode"
game_node = Node(game)
scene = init_screen(game_node)

LEFT = CartesianIndex(-1, 0)
RIGHT = CartesianIndex(1, 0)
UP = CartesianIndex(0, 1)
DOWN = CartesianIndex(0, -1)

actions = [rand([LEFT,RIGHT,UP,DOWN]) for _ in game.snakes]
is_exit = Ref{Bool}(false)

on(scene.events.keyboardbuttons) do but
if ispressed(but, Keyboard.left)
actions[1] != -LEFT && (actions[1] = LEFT)
elseif ispressed(but, Keyboard.up)
actions[1] != -UP && (actions[1] = UP)
elseif ispressed(but, Keyboard.down)
actions[1] != -DOWN && (actions[1] = DOWN)
elseif ispressed(but, Keyboard.right)
actions[1] != -RIGHT && (actions[1] = RIGHT)
elseif ispressed(but, Keyboard.a)
actions[2] != -LEFT && (actions[2] = LEFT)
elseif ispressed(but, Keyboard.w)
actions[2] != -UP && (actions[2] = UP)
elseif ispressed(but, Keyboard.s)
actions[2] != -DOWN && (actions[2] = DOWN)
elseif ispressed(but, Keyboard.d)
actions[2] != -RIGHT && (actions[2] = RIGHT)
elseif ispressed(but, Keyboard.j)
actions[3] != -LEFT && (actions[3] = LEFT)
elseif ispressed(but, Keyboard.i)
actions[3] != -UP && (actions[3] = UP)
elseif ispressed(but, Keyboard.k)
actions[3] != -DOWN && (actions[3] = DOWN)
elseif ispressed(but, Keyboard.l)
actions[3] != -RIGHT && (actions[3] = RIGHT)
elseif ispressed(but, Keyboard.q)
is_exit[] = true
end
end

record(scene, f_name; framerate=framerate) do io
while true
sleep(1)
is_success = game(actions)
game_node[] = game
recordframe!(io)
is_success || break
is_exit[] && break
end
end
println("game over")
end
124 changes: 124 additions & 0 deletions src/snake_game.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
export SnakeGame

using Random

struct SnakeGame{N,M,R<:AbstractRNG}
board::BitArray{M}
# TODO: using DataStructures: Deque ?
snakes::Vector{Vector{CartesianIndex{N}}}
foods::Set{CartesianIndex{N}}
rng::R
end

Base.size(g::SnakeGame) = size(g.board)[2:end]

ind_of_wall(game) = 2*length(game.snakes)+1
ind_of_food(game) = 2*length(game.snakes)+2

mark_wall!(game, ind) = game.board[ind_of_wall(game), ind] = true
mark_food!(game, ind) = game.board[ind_of_food(game), ind] = true
unmark_food!(game, ind) = game.board[ind_of_food(game), ind] = false
mark_snake_head!(game, ind, i) = game.board[i, ind] = true
unmark_snake_head!(game, ind, i) = game.board[i, ind] = false
mark_snake_body!(game, ind, i) = game.board[length(game.snakes)+i, ind] = true
unmark_snake_body!(game, ind, i) = game.board[length(game.snakes)+i, ind] = false

get_walls(game) = findall(isone, selectdim(game.board, 1, ind_of_wall(game)))

"""
SnakeGame(;kwargs...)
# Keyword Arguments
- `size::NTuple{N,Int}=(8,8)`, the size of game board. `N` can be greater than 2.
- `walls`, an iterable type with elements of type `CartesianIndex{N}`.
- `n_snakes`, number of snakes.
- `n_foods`, maximum number of foods in each step.
- `rng::AbstractRNG`, inner RNG used to sample initial snakes and necessary foods in each step.
"""
function SnakeGame(;size=(8,8), walls=[], n_snakes=1, n_foods=1,rng=Random.GLOBAL_RNG)
n_snakes+n_foods >= reduce(*, size) && error("n_snakes+n_foods must be less than the total grids")
board = BitArray(undef, n_snakes+n_snakes+1#=wall=#+1#=food=#, size...)
snakes = [Vector{CartesianIndex{length(size)}}() for _ in 1:n_snakes]
foods = Set{CartesianIndex{length(size)}}()
game = SnakeGame(board, snakes, foods, rng)

fill!(board, false)

for w in walls
mark_wall!(game, w)
end

while length(foods) < n_foods
p = rand(rng, CartesianIndices(size))
if any(@view(board[:, p]))
continue # there's a wall
else
push!(foods, p)
mark_food!(game, p)
end
end

for i in 1:n_snakes
while true
p = rand(rng, CartesianIndices(size))
if any(@view(board[:, p]))
continue # there's a wall or food
else
push!(snakes[i], p)
mark_snake_head!(game, p, i)
break
end
end
end

game
end

(game::SnakeGame)(action::CartesianIndex) = game([action])

function (game::SnakeGame{N})(actions::Vector{CartesianIndex{N}}) where N
# 1. move snake
for ((i, s), a) in zip(enumerate(game.snakes), actions)
unmark_snake_head!(game, s[1], i)
mark_snake_body!(game, s[1], i)
pushfirst!(s, CartesianIndex(mod.((s[1] + a).I, axes(game.board)[2:end])))
mark_snake_head!(game, s[1], i)
if s[1] in game.foods
unmark_food!(game, s[1])
else
unmark_snake_body!(game, s[end], i)
pop!(s)
end
end
# 2. check collision
for s in game.snakes
sum(@view(game.board[:, s[1]])) == 1 || return false
end
# 3. create new foods
for f in game.foods
if !game.board[ind_of_food(game), f]
# food is eaten
pop!(game.foods, f)
food = rand(game.rng, CartesianIndices(size(game)))
attempts = 1
while any(@view(game.board[:, food]))
food = rand(game.rng, CartesianIndices(size(game)))
attempts += 1
if attempts > reduce(*, size(game))
@warn "a rare case happened: sampled too many times to generate food"
empty_positions = findall(iszero, vec(any(game.board, dims=1)))
if length(empty_positions) == 0
return false
else
food = CartesianIndices(size(game.board)[2:end])[rand(game.rng, empty_positions)]
break
end
end
end
push!(game.foods, food)
mark_food!(game, food)
end
end
return true
end

2 comments on commit c241130

@findmyway
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/18021

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.0 -m "<description of version>" c241130f990e3b8ca1765947a91607710dc23524
git push origin v0.1.0

Please sign in to comment.