diff --git a/examples/flappylearning/.gitignore b/examples/flappylearning/.gitignore new file mode 100644 index 0000000000..dc22e61c94 --- /dev/null +++ b/examples/flappylearning/.gitignore @@ -0,0 +1 @@ +game diff --git a/examples/flappylearning/LICENSE b/examples/flappylearning/LICENSE new file mode 100644 index 0000000000..87cfb6f0d1 --- /dev/null +++ b/examples/flappylearning/LICENSE @@ -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. diff --git a/examples/flappylearning/README.md b/examples/flappylearning/README.md new file mode 100644 index 0000000000..57e288910f --- /dev/null +++ b/examples/flappylearning/README.md @@ -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 diff --git a/examples/flappylearning/game.v b/examples/flappylearning/game.v new file mode 100644 index 0000000000..65c10625de --- /dev/null +++ b/examples/flappylearning/game.v @@ -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() +} diff --git a/examples/flappylearning/img/background.png b/examples/flappylearning/img/background.png new file mode 100644 index 0000000000..22b307092c Binary files /dev/null and b/examples/flappylearning/img/background.png differ diff --git a/examples/flappylearning/img/bird.png b/examples/flappylearning/img/bird.png new file mode 100644 index 0000000000..ca3a15990b Binary files /dev/null and b/examples/flappylearning/img/bird.png differ diff --git a/examples/flappylearning/img/flappy.png b/examples/flappylearning/img/flappy.png new file mode 100644 index 0000000000..c7085aa8ad Binary files /dev/null and b/examples/flappylearning/img/flappy.png differ diff --git a/examples/flappylearning/img/pipebottom.png b/examples/flappylearning/img/pipebottom.png new file mode 100644 index 0000000000..99d27b2679 Binary files /dev/null and b/examples/flappylearning/img/pipebottom.png differ diff --git a/examples/flappylearning/img/pipetop.png b/examples/flappylearning/img/pipetop.png new file mode 100644 index 0000000000..1fcd52b3c4 Binary files /dev/null and b/examples/flappylearning/img/pipetop.png differ diff --git a/examples/flappylearning/modules/neuroevolution/neuronevolution.v b/examples/flappylearning/modules/neuroevolution/neuronevolution.v new file mode 100644 index 0000000000..a9422d1a3c --- /dev/null +++ b/examples/flappylearning/modules/neuroevolution/neuronevolution.v @@ -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() + }) +} +