examples: add flappylearning to examples (#7605)
parent
d563261e58
commit
e69e5c5b91
|
@ -0,0 +1 @@
|
|||
game
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 uxnow
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,16 @@
|
|||
# flappylearning-v
|
||||
flappy learning implemented by vlang
|
||||
|
||||
## get started
|
||||
|
||||
```sh
|
||||
v run game.v
|
||||
```
|
||||
|
||||
![flappy.png](img/flappy.png)
|
||||
|
||||
## thanks
|
||||
https://github.com/xviniette/FlappyLearning
|
||||
|
||||
## license
|
||||
MIT
|
|
@ -0,0 +1,275 @@
|
|||
|
||||
module main
|
||||
|
||||
import gg
|
||||
import gx
|
||||
import os
|
||||
import time
|
||||
import math
|
||||
import rand
|
||||
|
||||
import neuroevolution
|
||||
|
||||
const (
|
||||
win_width = 500
|
||||
win_height = 512
|
||||
timer_period = 24 // ms
|
||||
)
|
||||
|
||||
struct Bird {
|
||||
mut:
|
||||
x f64 = 80
|
||||
y f64 = 250
|
||||
width f64 = 40
|
||||
height f64 = 30
|
||||
|
||||
alive bool = true
|
||||
gravity f64
|
||||
velocity f64 = 0.3
|
||||
jump f64 = -6
|
||||
}
|
||||
|
||||
fn (mut b Bird) flap() {
|
||||
b.gravity = b.jump
|
||||
}
|
||||
|
||||
fn (mut b Bird) update() {
|
||||
b.gravity += b.velocity
|
||||
b.y += b.gravity
|
||||
}
|
||||
|
||||
fn (b Bird) is_dead(height f64, pipes []Pipe) bool {
|
||||
if b.y >= height || b.y + b.height <= 0 {
|
||||
return true
|
||||
}
|
||||
for pipe in pipes {
|
||||
if !(
|
||||
b.x > pipe.x + pipe.width ||
|
||||
b.x + b.width < pipe.x ||
|
||||
b.y > pipe.y + pipe.height ||
|
||||
b.y + b.height < pipe.y
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
struct Pipe {
|
||||
mut:
|
||||
x f64 = 80
|
||||
y f64 = 250
|
||||
width f64 = 40
|
||||
height f64 = 30
|
||||
speed f64 = 3
|
||||
}
|
||||
|
||||
fn (mut p Pipe) update() {
|
||||
p.x -= p.speed
|
||||
}
|
||||
|
||||
fn (p Pipe) is_out() bool {
|
||||
return p.x + p.width < 0
|
||||
}
|
||||
|
||||
struct App {
|
||||
mut:
|
||||
gg &gg.Context
|
||||
background gg.Image
|
||||
bird gg.Image
|
||||
pipetop gg.Image
|
||||
pipebottom gg.Image
|
||||
|
||||
pipes []Pipe
|
||||
birds []Bird
|
||||
score int
|
||||
max_score int
|
||||
width f64 = win_width
|
||||
height f64 = win_height
|
||||
spawn_interval f64 = 90
|
||||
interval f64
|
||||
|
||||
nv neuroevolution.Generations
|
||||
gen []neuroevolution.Network
|
||||
alives int
|
||||
generation int
|
||||
|
||||
background_speed f64 = 0.5
|
||||
background_x f64
|
||||
}
|
||||
|
||||
fn (mut app App) start() {
|
||||
app.interval = 0
|
||||
app.score = 0
|
||||
app.pipes = []
|
||||
app.birds = []
|
||||
app.gen = app.nv.generate()
|
||||
|
||||
for _ in 0 .. app.gen.len {
|
||||
app.birds << Bird{}
|
||||
}
|
||||
app.generation++
|
||||
app.alives = app.birds.len
|
||||
}
|
||||
|
||||
fn (app &App) is_it_end() bool {
|
||||
for i in 0 .. app.birds.len {
|
||||
if app.birds[i].alive {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn (mut app App) update() {
|
||||
app.background_x += app.background_speed
|
||||
mut next_holl := f64(0)
|
||||
|
||||
if app.birds.len > 0 {
|
||||
for i := 0; i < app.pipes.len; i += 2 {
|
||||
if app.pipes[i].x + app.pipes[i].width > app.birds[0].x {
|
||||
next_holl = app.pipes[i].height / app.height
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for mut j, bird in app.birds {
|
||||
if bird.alive {
|
||||
inputs := [
|
||||
bird.y / app.height,
|
||||
next_holl,
|
||||
]
|
||||
res := app.gen[j].compute(inputs)
|
||||
if res[0] > 0.5 {
|
||||
bird.flap()
|
||||
}
|
||||
|
||||
bird.update()
|
||||
|
||||
if bird.is_dead(app.height, app.pipes) {
|
||||
bird.alive = false
|
||||
app.alives--
|
||||
app.nv.network_score(app.gen[j], app.score)
|
||||
if app.is_it_end() {
|
||||
app.start()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for k := 0; k < app.pipes.len; k++ {
|
||||
app.pipes[k].update()
|
||||
if app.pipes[k].is_out() {
|
||||
app.pipes.delete(k)
|
||||
k--
|
||||
}
|
||||
}
|
||||
|
||||
if app.interval == 0 {
|
||||
delta_bord := f64(50)
|
||||
pipe_holl := f64(120)
|
||||
holl_position := math.round(rand.f64() * (app.height - delta_bord * 2.0 - pipe_holl)) + delta_bord
|
||||
app.pipes << Pipe{
|
||||
x: app.width
|
||||
y: 0
|
||||
height: holl_position
|
||||
}
|
||||
|
||||
app.pipes << Pipe{
|
||||
x: app.width
|
||||
y: holl_position + pipe_holl
|
||||
height: app.height
|
||||
}
|
||||
}
|
||||
|
||||
app.interval++
|
||||
|
||||
if app.interval == app.spawn_interval {
|
||||
app.interval = 0
|
||||
}
|
||||
|
||||
app.score++
|
||||
app.max_score = if app.score > app.max_score {
|
||||
app.score
|
||||
} else {
|
||||
app.max_score
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut app := &App{
|
||||
gg: 0
|
||||
}
|
||||
app.gg = gg.new_context({
|
||||
bg_color: gx.white
|
||||
width: win_width
|
||||
height: win_height
|
||||
use_ortho: true // This is needed for 2D drawing
|
||||
create_window: true
|
||||
window_title: 'flappylearning-v'
|
||||
frame_fn: frame
|
||||
user_data: app
|
||||
init_fn: init_images
|
||||
font_path: os.resource_abs_path('../assets/fonts/RobotoMono-Regular.ttf')
|
||||
})
|
||||
app.nv = neuroevolution.Generations{
|
||||
population: 50
|
||||
network: [2, 2, 1]
|
||||
}
|
||||
app.start()
|
||||
go app.run()
|
||||
app.gg.run()
|
||||
}
|
||||
|
||||
fn (mut app App) run() {
|
||||
for {
|
||||
app.update()
|
||||
time.sleep_ms(timer_period)
|
||||
}
|
||||
}
|
||||
|
||||
fn init_images(mut app App) {
|
||||
app.background = app.gg.create_image(os.resource_abs_path('./img/background.png'))
|
||||
app.bird = app.gg.create_image(os.resource_abs_path('./img/bird.png'))
|
||||
app.pipetop = app.gg.create_image(os.resource_abs_path('./img/pipetop.png'))
|
||||
app.pipebottom = app.gg.create_image(os.resource_abs_path('./img/pipebottom.png'))
|
||||
}
|
||||
|
||||
fn frame(app &App) {
|
||||
app.gg.begin()
|
||||
app.draw()
|
||||
app.gg.end()
|
||||
}
|
||||
|
||||
fn (app &App) display() {
|
||||
for i := 0; i < int(math.ceil(app.width / app.background.width) + 1.0); i++ {
|
||||
background_x := i * app.background.width - math.floor(int(app.background_x) % int(app.background.width))
|
||||
app.gg.draw_image(f32(background_x), 0, app.background.width, app.background.height, app.background)
|
||||
}
|
||||
|
||||
for i, pipe in app.pipes {
|
||||
if i % 2 == 0 {
|
||||
app.gg.draw_image(f32(pipe.x), f32(pipe.y + pipe.height - app.pipetop.height), app.pipetop.width, app.pipetop.height, app.pipetop)
|
||||
} else {
|
||||
app.gg.draw_image(f32(pipe.x), f32(pipe.y), app.pipebottom.width, app.pipebottom.height, app.pipebottom)
|
||||
}
|
||||
}
|
||||
|
||||
for bird in app.birds {
|
||||
if bird.alive {
|
||||
app.gg.draw_image(f32(bird.x), f32(bird.y), app.bird.width, app.bird.height, app.bird)
|
||||
}
|
||||
}
|
||||
app.gg.draw_text_def(10 ,25, 'Score: $app.score')
|
||||
app.gg.draw_text_def(10 ,50, 'Max Score: $app.max_score')
|
||||
app.gg.draw_text_def(10 ,75, 'Generation: $app.generation')
|
||||
app.gg.draw_text_def(10 ,100, 'Alive: $app.alives / $app.nv.population')
|
||||
}
|
||||
|
||||
fn (app &App) draw() {
|
||||
app.display()
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 382 B |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,329 @@
|
|||
|
||||
module neuroevolution
|
||||
|
||||
import rand
|
||||
import math
|
||||
|
||||
fn random_clamped() f64 {
|
||||
return rand.f64() * 2 - 1
|
||||
}
|
||||
|
||||
pub fn activation(a f64) f64 {
|
||||
ap := (-a) / 1
|
||||
return (1 / (1 + math.exp(ap)))
|
||||
}
|
||||
|
||||
fn round(a int, b f64) int {
|
||||
return int(math.round(f64(a) * b))
|
||||
}
|
||||
|
||||
struct Neuron {
|
||||
mut:
|
||||
value f64
|
||||
weights []f64
|
||||
}
|
||||
|
||||
fn (mut n Neuron) populate(nb int) {
|
||||
for _ in 0 .. nb {
|
||||
n.weights << random_clamped()
|
||||
}
|
||||
}
|
||||
|
||||
struct Layer {
|
||||
id int
|
||||
mut:
|
||||
neurons []Neuron
|
||||
}
|
||||
|
||||
fn (mut l Layer) populate(nb_neurons int, nb_inputs int) {
|
||||
for _ in 0 .. nb_neurons {
|
||||
mut n := Neuron{}
|
||||
n.populate(nb_inputs)
|
||||
l.neurons << n
|
||||
}
|
||||
}
|
||||
|
||||
struct Network {
|
||||
mut:
|
||||
layers []Layer
|
||||
}
|
||||
|
||||
fn (mut n Network) populate(network []int) {
|
||||
|
||||
assert network.len >= 2
|
||||
input := network[0]
|
||||
hiddens := network.slice(1, network.len - 1)
|
||||
output := network[network.len - 1]
|
||||
|
||||
mut index := 0
|
||||
mut previous_neurons := 0
|
||||
mut input_layer := Layer{
|
||||
id: index
|
||||
}
|
||||
input_layer.populate(input, previous_neurons)
|
||||
n.layers << input_layer
|
||||
|
||||
previous_neurons = input
|
||||
index++
|
||||
for hidden in hiddens {
|
||||
mut hidden_layer := Layer{
|
||||
id: index
|
||||
}
|
||||
hidden_layer.populate(hidden, previous_neurons)
|
||||
previous_neurons = hidden
|
||||
n.layers << hidden_layer
|
||||
index++
|
||||
}
|
||||
|
||||
mut output_layer := Layer{
|
||||
id: index
|
||||
}
|
||||
output_layer.populate(output, previous_neurons)
|
||||
n.layers << output_layer
|
||||
}
|
||||
|
||||
fn (n Network) get_save() Save {
|
||||
|
||||
mut save := Save{}
|
||||
for layer in n.layers {
|
||||
save.neurons << layer.neurons.len
|
||||
for neuron in layer.neurons {
|
||||
for weight in neuron.weights {
|
||||
save.weights << weight
|
||||
}
|
||||
}
|
||||
}
|
||||
return save
|
||||
}
|
||||
|
||||
fn (mut n Network) set_save(save Save) {
|
||||
|
||||
mut previous_neurons := 0
|
||||
mut index := 0
|
||||
mut index_weights := 0
|
||||
|
||||
n.layers = []
|
||||
for save_neuron in save.neurons {
|
||||
mut layer := Layer{
|
||||
id: index
|
||||
}
|
||||
layer.populate(save_neuron, previous_neurons)
|
||||
for mut neuron in layer.neurons {
|
||||
for i in 0 .. neuron.weights.len {
|
||||
neuron.weights[i] = save.weights[index_weights]
|
||||
index_weights++
|
||||
}
|
||||
}
|
||||
previous_neurons = save_neuron
|
||||
index++
|
||||
n.layers << layer
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut n Network) compute(inputs []f64) []f64 {
|
||||
assert n.layers.len > 0
|
||||
assert inputs.len == n.layers[0].neurons.len
|
||||
|
||||
for i, input in inputs {
|
||||
n.layers[0].neurons[i].value = input
|
||||
}
|
||||
|
||||
mut prev_layer := n.layers[0]
|
||||
|
||||
for i in 1 .. n.layers.len {
|
||||
for j, neuron in n.layers[i].neurons {
|
||||
mut sum := f64(0)
|
||||
for k, prev_layer_neuron in prev_layer.neurons {
|
||||
sum += prev_layer_neuron.value * neuron.weights[k]
|
||||
}
|
||||
n.layers[i].neurons[j].value = activation(sum)
|
||||
}
|
||||
prev_layer = n.layers[i]
|
||||
}
|
||||
|
||||
mut outputs := []f64{}
|
||||
mut last_layer := n.layers[n.layers.len - 1]
|
||||
for neuron in last_layer.neurons {
|
||||
outputs << neuron.value
|
||||
}
|
||||
|
||||
return outputs
|
||||
}
|
||||
|
||||
struct Save {
|
||||
mut:
|
||||
neurons []int
|
||||
weights []f64
|
||||
}
|
||||
|
||||
fn (s Save) clone() Save {
|
||||
mut save := Save{}
|
||||
save.neurons << s.neurons
|
||||
save.weights << s.weights
|
||||
return save
|
||||
}
|
||||
|
||||
struct Genome {
|
||||
score int
|
||||
network Save
|
||||
}
|
||||
|
||||
struct Generation {
|
||||
mut:
|
||||
genomes []Genome
|
||||
}
|
||||
|
||||
fn (mut g Generation) add_genome(genome Genome) {
|
||||
|
||||
mut i := 0
|
||||
|
||||
for gg in g.genomes {
|
||||
if genome.score > gg.score {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
g.genomes.insert(i, genome)
|
||||
}
|
||||
|
||||
fn (g1 Genome) breed(g2 Genome, nb_child int) []Save {
|
||||
mut datas := []Save{}
|
||||
|
||||
for _ in 0 .. nb_child {
|
||||
|
||||
mut data := g1.network.clone()
|
||||
|
||||
for i, weight in g2.network.weights {
|
||||
if rand.f64() <= 0.5 {
|
||||
data.weights[i] = weight
|
||||
}
|
||||
}
|
||||
|
||||
for i, _ in data.weights {
|
||||
if rand.f64() <= 0.1 {
|
||||
data.weights[i] += (rand.f64() * 2 - 1) * 0.5
|
||||
}
|
||||
}
|
||||
|
||||
datas << data
|
||||
}
|
||||
|
||||
return datas
|
||||
}
|
||||
|
||||
fn (g Generation) next(population int) []Save {
|
||||
|
||||
mut nexts := []Save{}
|
||||
|
||||
if population == 0 {
|
||||
return nexts
|
||||
}
|
||||
|
||||
keep := round(population, 0.2)
|
||||
|
||||
for i in 0 .. keep {
|
||||
if nexts.len < population {
|
||||
nexts << g.genomes[i].network.clone()
|
||||
}
|
||||
}
|
||||
|
||||
random := round(population, 0.2)
|
||||
|
||||
for _ in 0 .. random {
|
||||
|
||||
if nexts.len < population {
|
||||
mut n := g.genomes[0].network.clone()
|
||||
for k, _ in n.weights {
|
||||
n.weights[k] = random_clamped()
|
||||
}
|
||||
nexts << n
|
||||
}
|
||||
}
|
||||
|
||||
mut max := 0
|
||||
out: for {
|
||||
for i in 0 .. max {
|
||||
mut childs := g.genomes[i].breed(g.genomes[max], 1)
|
||||
for c in childs {
|
||||
nexts << c
|
||||
if nexts.len >= population {
|
||||
break out
|
||||
}
|
||||
}
|
||||
}
|
||||
max++
|
||||
if max >= g.genomes.len - 1 {
|
||||
max = 0
|
||||
}
|
||||
}
|
||||
|
||||
return nexts
|
||||
}
|
||||
|
||||
pub struct Generations {
|
||||
pub:
|
||||
population int
|
||||
network []int
|
||||
mut:
|
||||
generations []Generation
|
||||
}
|
||||
|
||||
fn (mut gs Generations) first() []Save {
|
||||
mut out := []Save{}
|
||||
for _ in 0 .. gs.population {
|
||||
mut nn := Network{}
|
||||
nn.populate(gs.network)
|
||||
out << nn.get_save()
|
||||
}
|
||||
|
||||
gs.generations << Generation{}
|
||||
return out
|
||||
}
|
||||
|
||||
fn (mut gs Generations) next() []Save {
|
||||
assert gs.generations.len > 0
|
||||
gen := gs.generations[gs.generations.len - 1].next(gs.population)
|
||||
gs.generations << Generation{}
|
||||
return gen
|
||||
}
|
||||
|
||||
fn (mut gs Generations) add_genome(genome Genome) {
|
||||
assert gs.generations.len > 0
|
||||
gs.generations[gs.generations.len - 1].add_genome(genome)
|
||||
}
|
||||
|
||||
fn (mut gs Generations) restart() {
|
||||
gs.generations = []
|
||||
}
|
||||
|
||||
pub fn (mut gs Generations) generate() []Network {
|
||||
|
||||
saves := if gs.generations.len == 0 {
|
||||
gs.first()
|
||||
} else {
|
||||
gs.next()
|
||||
}
|
||||
|
||||
mut nns := []Network{}
|
||||
for save in saves {
|
||||
mut nn := Network{}
|
||||
nn.set_save(save)
|
||||
nns << nn
|
||||
}
|
||||
|
||||
if gs.generations.len >= 2 {
|
||||
gs.generations.delete(0)
|
||||
}
|
||||
|
||||
return nns
|
||||
}
|
||||
|
||||
pub fn (mut gs Generations) network_score(network Network, score int) {
|
||||
gs.add_genome(Genome{
|
||||
score: score
|
||||
network: network.get_save()
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue