Created
January 4, 2026 18:38
-
-
Save Hammer2900/38bd79df46edc2ae7b69ad3243ca2a27 to your computer and use it in GitHub Desktop.
neat snake test map
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import "core:encoding/json" | |
| import "core:fmt" | |
| import "core:math" | |
| import "core:math/rand" | |
| import "core:os" | |
| import "core:slice" | |
| import "core:strings" | |
| import "vendor:raylib" | |
| // ============================================================================ | |
| // PART 1: NEAT LIBRARY | |
| // ============================================================================ | |
| NodeType :: enum { | |
| Input, | |
| Hidden, | |
| Output, | |
| Bias, | |
| } | |
| NodeGene :: struct { | |
| id: int, | |
| type: NodeType, | |
| layer: f32, | |
| value: f64, | |
| } | |
| ConnectionGene :: struct { | |
| in_node: int, | |
| out_node: int, | |
| weight: f64, | |
| enabled: bool, | |
| } | |
| Genome :: struct { | |
| id: int, | |
| fitness: f64, | |
| inputs_count: int, | |
| outputs_count: int, | |
| nodes: map[int]NodeGene, | |
| connections: [dynamic]ConnectionGene, | |
| sorted_nodes: [dynamic]^NodeGene, | |
| } | |
| free_genome :: proc(g: ^Genome) { | |
| delete(g.nodes) | |
| delete(g.connections) | |
| delete(g.sorted_nodes) | |
| } | |
| genome_compile :: proc(g: ^Genome) { | |
| clear(&g.sorted_nodes) | |
| for _, &node in g.nodes { | |
| append(&g.sorted_nodes, &node) | |
| } | |
| slice.sort_by_cmp(g.sorted_nodes[:], proc(a, b: ^NodeGene) -> slice.Ordering { | |
| if a.layer < b.layer do return .Less | |
| if a.layer > b.layer do return .Greater | |
| return .Equal | |
| }) | |
| } | |
| sigmoid :: proc(x: f64) -> f64 { | |
| return 1.0 / (1.0 + math.exp(-4.9 * x)) | |
| } | |
| genome_feedforward :: proc(g: ^Genome, inputs: []f64) -> []f64 { | |
| for node_ptr in g.sorted_nodes { | |
| node_ptr.value = 0.0 | |
| } | |
| for i in 0 ..< len(inputs) { | |
| if i in g.nodes { | |
| n := &g.nodes[i] | |
| n.value = inputs[i] | |
| } | |
| } | |
| for node in g.sorted_nodes { | |
| if node.type == .Input do continue | |
| total : f64 = 0 | |
| for conn in g.connections { | |
| if conn.out_node == node.id && conn.enabled { | |
| if in_n, ok := g.nodes[conn.in_node]; ok { | |
| total += in_n.value * conn.weight | |
| } | |
| } | |
| } | |
| node.value = sigmoid(total) | |
| } | |
| result := make([]f64, g.outputs_count, context.temp_allocator) | |
| out_idx := 0 | |
| for n in g.sorted_nodes { | |
| if n.type == .Output { | |
| if out_idx < len(result) { | |
| result[out_idx] = n.value | |
| out_idx += 1 | |
| } | |
| } | |
| } | |
| return result | |
| } | |
| // --- RANDOM GENERATION (NEW) --- | |
| create_random_genome :: proc(id, inputs, outputs: int) -> ^Genome { | |
| g := new(Genome) | |
| g.id = id | |
| g.inputs_count = inputs | |
| g.outputs_count = outputs | |
| g.nodes = make(map[int]NodeGene) | |
| // 1. Create Inputs | |
| for i in 0 ..< inputs { | |
| g.nodes[i] = NodeGene{ | |
| id = i, | |
| type = .Input, | |
| layer = 0.0, | |
| value = 0.0, | |
| } | |
| } | |
| // 2. Create Outputs | |
| // В NEAT-Python ID выходов обычно идут сразу после входов | |
| for i in 0 ..< outputs { | |
| node_id := inputs + i | |
| g.nodes[node_id] = NodeGene{ | |
| id = node_id, | |
| type = .Output, | |
| layer = 1.0, // Output layer | |
| value = 0.0, | |
| } | |
| } | |
| // 3. Dense Connections (Full mesh) | |
| // Соединяем каждый вход с каждым выходом случайным весом | |
| for i in 0 ..< inputs { | |
| for j in 0 ..< outputs { | |
| out_id := inputs + j | |
| weight := rand.float64_range(-2.0, 2.0) | |
| append(&g.connections, ConnectionGene{ | |
| in_node = i, | |
| out_node = out_id, | |
| weight = weight, | |
| enabled = true, | |
| }) | |
| } | |
| } | |
| genome_compile(g) | |
| return g | |
| } | |
| create_random_population :: proc(size, inputs, outputs: int) -> [dynamic]^Genome { | |
| pop := make([dynamic]^Genome) | |
| for i in 0 ..< size { | |
| append(&pop, create_random_genome(i, inputs, outputs)) | |
| } | |
| return pop | |
| } | |
| // --- SAVE SYSTEM (NEW) --- | |
| save_population_to_file :: proc(pop: [dynamic]^Genome, filename: string) { | |
| sb := strings.builder_make() | |
| defer strings.builder_destroy(&sb) | |
| strings.write_string(&sb, "{\"population\": [\n") | |
| for g, i in pop { | |
| fmt.sbprintf(&sb, " {{\n \"key\": %d,\n \"fitness\": %f,\n", g.id, g.fitness) | |
| // Nodes | |
| strings.write_string(&sb, " \"nodes\": [\n") | |
| // Need to iterate map cleanly. Convert to slice for index control or just use counter | |
| node_count := len(g.nodes) | |
| k := 0 | |
| for _, node in g.nodes { | |
| type_str := "hidden" | |
| if node.type == .Input do type_str = "input" | |
| if node.type == .Output do type_str = "output" | |
| fmt.sbprintf(&sb, " {{\"id\": %d, \"type\": \"%s\", \"layer\": %f}}", node.id, type_str, node.layer) | |
| if k < node_count - 1 do strings.write_string(&sb, ",") | |
| strings.write_string(&sb, "\n") | |
| k += 1 | |
| } | |
| strings.write_string(&sb, " ],\n") | |
| // Connections | |
| strings.write_string(&sb, " \"connections\": [\n") | |
| conn_count := len(g.connections) | |
| for c, j in g.connections { | |
| enabled_str := c.enabled ? "true" : "false" | |
| fmt.sbprintf(&sb, " {{\"in\": %d, \"out\": %d, \"weight\": %f, \"enabled\": %s}}", c.in_node, c.out_node, c.weight, enabled_str) | |
| if j < conn_count - 1 do strings.write_string(&sb, ",") | |
| strings.write_string(&sb, "\n") | |
| } | |
| strings.write_string(&sb, " ]\n") | |
| strings.write_string(&sb, " }") | |
| if i < len(pop) - 1 do strings.write_string(&sb, ",") | |
| strings.write_string(&sb, "\n") | |
| } | |
| strings.write_string(&sb, "]}") | |
| os.write_entire_file(filename, transmute([]u8)strings.to_string(sb)) | |
| fmt.println("Population SAVED to", filename) | |
| } | |
| // --- JSON Helpers --- | |
| json_to_f64 :: proc(v: json.Value) -> f64 { | |
| switch t in v { | |
| case json.Float: return t | |
| case json.Integer: return f64(t) | |
| case json.Null, json.Boolean, json.String, json.Array, json.Object: return 0.0 | |
| } | |
| return 0.0 | |
| } | |
| json_to_int :: proc(v: json.Value) -> int { | |
| switch t in v { | |
| case json.Integer: return int(t) | |
| case json.Float: return int(t) | |
| case json.Null, json.Boolean, json.String, json.Array, json.Object: return 0 | |
| } | |
| return 0 | |
| } | |
| json_to_string :: proc(v: json.Value) -> string { | |
| switch t in v { | |
| case json.String: return t | |
| case json.Null, json.Integer, json.Float, json.Boolean, json.Array, json.Object: return "" | |
| } | |
| return "" | |
| } | |
| json_to_bool :: proc(v: json.Value) -> bool { | |
| switch t in v { | |
| case json.Boolean: return t | |
| case json.Null, json.Integer, json.Float, json.String, json.Array, json.Object: return false | |
| } | |
| return false | |
| } | |
| load_population_from_file :: proc(filename: string, inputs, outputs: int) -> [dynamic]^Genome { | |
| if !os.exists(filename) { | |
| return nil | |
| } | |
| data, ok := os.read_entire_file(filename) | |
| if !ok { | |
| return nil | |
| } | |
| defer delete(data) | |
| json_data, err := json.parse(data) | |
| if err != .None { | |
| return nil | |
| } | |
| defer json.destroy_value(json_data) | |
| root := json_data.(json.Object) | |
| pop_array: json.Array | |
| if "population" in root { | |
| pop_array = root["population"].(json.Array) | |
| } else if "pop" in root { | |
| pop_array = root["pop"].(json.Array) | |
| } else { | |
| return nil | |
| } | |
| genomes := make([dynamic]^Genome) | |
| for val in pop_array { | |
| g_obj := val.(json.Object) | |
| new_g := new(Genome) | |
| new_g.inputs_count = inputs | |
| new_g.outputs_count = outputs | |
| if "key" in g_obj { | |
| new_g.id = json_to_int(g_obj["key"]) | |
| } else if "id" in g_obj { | |
| new_g.id = json_to_int(g_obj["id"]) | |
| } | |
| if "fitness" in g_obj { | |
| new_g.fitness = json_to_f64(g_obj["fitness"]) | |
| } | |
| new_g.nodes = make(map[int]NodeGene) | |
| nodes_arr := g_obj["nodes"].(json.Array) | |
| for n_val in nodes_arr { | |
| n_obj := n_val.(json.Object) | |
| id := json_to_int(n_obj["id"]) | |
| type_str := json_to_string(n_obj["type"]) | |
| layer := f32(json_to_f64(n_obj["layer"])) | |
| nt: NodeType | |
| switch type_str { | |
| case "input": nt = .Input | |
| case "output": nt = .Output | |
| case: nt = .Hidden | |
| } | |
| new_g.nodes[id] = NodeGene{ | |
| id = id, | |
| type = nt, | |
| layer = layer, | |
| value = 0, | |
| } | |
| } | |
| conns_arr := g_obj["connections"].(json.Array) | |
| new_g.connections = make([dynamic]ConnectionGene, 0, len(conns_arr)) | |
| for c_val in conns_arr { | |
| c_obj := c_val.(json.Object) | |
| conn := ConnectionGene{ | |
| in_node = json_to_int(c_obj["in"]), | |
| out_node = json_to_int(c_obj["out"]), | |
| weight = json_to_f64(c_obj["weight"]), | |
| enabled = json_to_bool(c_obj["enabled"]), | |
| } | |
| append(&new_g.connections, conn) | |
| } | |
| genome_compile(new_g) | |
| append(&genomes, new_g) | |
| } | |
| return genomes | |
| } | |
| // ============================================================================ | |
| // PART 2: SNAKE DEMO | |
| // ============================================================================ | |
| CELL_SIZE :: 12 | |
| GRID_WIDTH :: 100 | |
| GRID_HEIGHT :: 100 | |
| SCREEN_W :: 1600 | |
| SCREEN_H :: 900 | |
| DEATH_SPEED :: 0.05 | |
| BASE_FOOD :: 50 | |
| SNAKE_COUNT :: 160 | |
| Direction :: enum { | |
| Up, Right, Down, Left | |
| } | |
| SnakeState :: enum { | |
| Alive, | |
| DyingShrink, | |
| DyingRot, | |
| } | |
| Point :: struct { | |
| x, y: int | |
| } | |
| DemoSnake :: struct { | |
| genome: ^Genome, | |
| body: [dynamic]Point, | |
| dir: Direction, | |
| stamina: int, | |
| color: raylib.Color, | |
| scale: f32, | |
| state: SnakeState, | |
| score: int, | |
| } | |
| Game :: struct { | |
| population: [dynamic]^Genome, | |
| snakes: [dynamic]DemoSnake, | |
| foods: [dynamic]Point, | |
| camera_pos: [2]f32, | |
| mode: int, | |
| msg_timer: f32, | |
| } | |
| init_snake :: proc(g: ^Genome) -> DemoSnake { | |
| pad :: 5 | |
| sx := rand.int_max(GRID_WIDTH - 2 * pad) + pad | |
| sy := rand.int_max(GRID_HEIGHT - 2 * pad) + pad | |
| s := DemoSnake{ | |
| genome = g, | |
| dir = .Up, | |
| stamina = 400, | |
| color = raylib.Color{ | |
| u8(rand.int_max(155) + 100), | |
| u8(rand.int_max(155) + 100), | |
| u8(rand.int_max(155) + 100), | |
| 255, | |
| }, | |
| scale = 1.0, | |
| state = .Alive, | |
| body = make([dynamic]Point), | |
| } | |
| append(&s.body, Point{ sx, sy }) | |
| append(&s.body, Point{ sx, sy + 1 }) | |
| append(&s.body, Point{ sx, sy + 2 }) | |
| return s | |
| } | |
| destroy_snake :: proc(s: ^DemoSnake) { | |
| delete(s.body) | |
| } | |
| die_snake :: proc(s: ^DemoSnake) { | |
| if s.state != .Alive do return | |
| if len(s.body) > 2 && rand.float32() < 0.3 { | |
| s.state = .DyingRot | |
| s.scale = 1.2 | |
| } else { | |
| s.state = .DyingShrink | |
| } | |
| } | |
| update_visuals :: proc(s: ^DemoSnake, foods: ^[dynamic]Point) -> bool { | |
| if s.state == .Alive do return false | |
| if s.state == .DyingShrink { | |
| s.scale -= DEATH_SPEED | |
| if s.scale <= 0 do return true | |
| } else if s.state == .DyingRot { | |
| s.scale -= DEATH_SPEED * 2.0 | |
| if s.scale <= 0.8 { | |
| for segment in s.body { | |
| exists := false | |
| for f in foods { | |
| if f == segment { | |
| exists = true; break | |
| } | |
| } | |
| if !exists { | |
| append(foods, segment) | |
| } | |
| } | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| think :: proc(s: ^DemoSnake, occupied: map[Point]bool, foods: [dynamic]Point) { | |
| if s.state != .Alive do return | |
| head := s.body[0] | |
| closest : Point = { -1, -1 } | |
| min_dist_sq := f32(999999.0) | |
| for f in foods { | |
| dx := f32(f.x - head.x) | |
| dy := f32(f.y - head.y) | |
| d_sq := dx * dx + dy * dy | |
| if d_sq < min_dist_sq { | |
| min_dist_sq = d_sq | |
| closest = f | |
| } | |
| } | |
| inputs := make([dynamic]f64, 0, 24, context.temp_allocator) | |
| // A. Danger | |
| dirs := [4]Point{ { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 } } | |
| for d in dirs { | |
| nx, ny := head.x + d.x, head.y + d.y | |
| danger := 0.0 | |
| if nx < 0 || nx >= GRID_WIDTH || ny < 0 || ny >= GRID_HEIGHT || (Point{ nx, ny } in occupied) { | |
| danger = 1.0 | |
| } | |
| append(&inputs, danger) | |
| } | |
| // B. Food Direction | |
| if closest.x != -1 { | |
| dx := closest.x - head.x | |
| dy := closest.y - head.y | |
| append(&inputs, dy < 0 ? 1.0 : 0.0) | |
| append(&inputs, dx > 0 ? 1.0 : 0.0) | |
| append(&inputs, dy > 0 ? 1.0 : 0.0) | |
| append(&inputs, dx < 0 ? 1.0 : 0.0) | |
| } else { | |
| append(&inputs, 0, 0, 0, 0) | |
| } | |
| // C. Radar | |
| radars := [8]Point{ { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 } } | |
| for r in radars { | |
| cx, cy := head.x, head.y | |
| dist := 0.0 | |
| found := false | |
| for _ in 0 ..< 15 { | |
| cx += r.x | |
| cy += r.y | |
| dist += 1.0 | |
| if cx < 0 || cx >= GRID_WIDTH || cy < 0 || cy >= GRID_HEIGHT || (Point{ cx, cy } in occupied) { | |
| found = true | |
| break | |
| } | |
| } | |
| append(&inputs, found ? 1.0 / dist : 0.0) | |
| } | |
| // D. Direction | |
| append(&inputs, s.dir == .Up ? 1.0 : 0.0) | |
| append(&inputs, s.dir == .Right ? 1.0 : 0.0) | |
| append(&inputs, s.dir == .Down ? 1.0 : 0.0) | |
| append(&inputs, s.dir == .Left ? 1.0 : 0.0) | |
| // E. Coords | |
| append(&inputs, f64(head.y) / f64(GRID_HEIGHT)) | |
| append(&inputs, f64(GRID_WIDTH - head.x) / f64(GRID_WIDTH)) | |
| append(&inputs, f64(GRID_HEIGHT - head.y) / f64(GRID_HEIGHT)) | |
| append(&inputs, f64(head.x) / f64(GRID_WIDTH)) | |
| outputs := genome_feedforward(s.genome, inputs[:]) | |
| best_idx := -1 | |
| max_val := -999.0 | |
| current_idx := int(s.dir) | |
| opposite := (current_idx + 2) % 4 | |
| limit := min(4, len(outputs)) | |
| for i in 0 ..< limit { | |
| if i == opposite do continue | |
| if outputs[i] > max_val { | |
| max_val = outputs[i] | |
| best_idx = i | |
| } | |
| } | |
| if best_idx != -1 { | |
| s.dir = Direction(best_idx) | |
| } | |
| } | |
| move_snake :: proc(s: ^DemoSnake, occupied: map[Point]bool, foods: ^[dynamic]Point) { | |
| if s.state != .Alive do return | |
| head := s.body[0] | |
| next_pos := head | |
| switch s.dir { | |
| case .Up: next_pos.y -= 1 | |
| case .Right: next_pos.x += 1 | |
| case .Down: next_pos.y += 1 | |
| case .Left: next_pos.x -= 1 | |
| } | |
| if next_pos.x < 0 || next_pos.x >= GRID_WIDTH || | |
| next_pos.y < 0 || next_pos.y >= GRID_HEIGHT || | |
| (next_pos in occupied) { | |
| die_snake(s) | |
| return | |
| } | |
| inject_at(&s.body, 0, next_pos) | |
| s.stamina -= 1 | |
| ate := false | |
| for i in 0 ..< len(foods) { | |
| if foods[i] == next_pos { | |
| unordered_remove(foods, i) | |
| ate = true | |
| s.score += 1 | |
| s.stamina += 150 | |
| if s.stamina > 500 do s.stamina = 500 | |
| break | |
| } | |
| } | |
| if !ate { | |
| pop(&s.body) | |
| } | |
| if s.stamina <= 0 { | |
| die_snake(s) | |
| } | |
| } | |
| restart_game :: proc(g: ^Game) { | |
| for &s in g.snakes { | |
| destroy_snake(&s) | |
| } | |
| clear(&g.snakes) | |
| clear(&g.foods) | |
| for _ in 0 ..< BASE_FOOD { | |
| append(&g.foods, Point{ rand.int_max(GRID_WIDTH), rand.int_max(GRID_HEIGHT) }) | |
| } | |
| if g.mode == 1 { | |
| top_k := 3 | |
| if len(g.population) < 3 do top_k = len(g.population) | |
| fmt.println("Restarting in ELITE mode") | |
| for i in 0 ..< SNAKE_COUNT { | |
| if len(g.population) > 0 { | |
| gen := g.population[i % top_k] | |
| append(&g.snakes, init_snake(gen)) | |
| } | |
| } | |
| } else { | |
| fmt.println("Restarting in CHAOS mode") | |
| for i in 0 ..< SNAKE_COUNT { | |
| if len(g.population) > 0 { | |
| idx := rand.int_max(len(g.population)) | |
| gen := g.population[idx] | |
| append(&g.snakes, init_snake(gen)) | |
| } | |
| } | |
| } | |
| } | |
| // ============================================================================ | |
| // MAIN | |
| // ============================================================================ | |
| main :: proc() { | |
| raylib.InitWindow(SCREEN_W, SCREEN_H, "NEAT Snake: Odin Implementation") | |
| defer raylib.CloseWindow() | |
| raylib.SetTargetFPS(60) | |
| game := new(Game) | |
| game.mode = 1 | |
| // 1. Попытка загрузить файл | |
| fmt.println("Checking for neat_snake_battle.json...") | |
| loaded_pop := load_population_from_file("neat_snake_battle.json", 24, 4) | |
| if loaded_pop != nil && len(loaded_pop) > 0 { | |
| fmt.println("Loaded population from file.") | |
| game.population = loaded_pop | |
| } else { | |
| fmt.println("File not found or invalid. Creating RANDOM population.") | |
| // Создаем случайную популяцию (24 входа, 4 выхода) | |
| game.population = create_random_population(100, 24, 4) | |
| } | |
| // Сортировка по фитнесу (если он есть) | |
| slice.sort_by_cmp(game.population[:], proc(a, b: ^Genome) -> slice.Ordering { | |
| if a.fitness > b.fitness do return .Less | |
| if a.fitness < b.fitness do return .Greater | |
| return .Equal | |
| }) | |
| restart_game(game) | |
| for !raylib.WindowShouldClose() { | |
| dt := raylib.GetFrameTime() | |
| if game.msg_timer > 0 do game.msg_timer -= dt | |
| // Inputs | |
| if raylib.IsMouseButtonDown(.LEFT) { | |
| delta := raylib.GetMouseDelta() | |
| game.camera_pos.x += delta.x | |
| game.camera_pos.y += delta.y | |
| } | |
| if raylib.IsKeyPressed(.ONE) { | |
| game.mode = 1; restart_game(game) | |
| } | |
| if raylib.IsKeyPressed(.TWO) { | |
| game.mode = 2; restart_game(game) | |
| } | |
| if raylib.IsKeyPressed(.R) { | |
| restart_game(game) | |
| } | |
| // SAVE BUTTON | |
| if raylib.IsKeyPressed(.S) { | |
| save_population_to_file(game.population, "neat_snake_battle.json") | |
| game.msg_timer = 2.0 | |
| } | |
| occupied := make(map[Point]bool, 1000, context.temp_allocator) | |
| alive_count := 0 | |
| for &s in game.snakes { | |
| if s.state == .Alive { | |
| alive_count += 1 | |
| for p in s.body { | |
| occupied[p] = true | |
| } | |
| } | |
| } | |
| for i := len(game.snakes) - 1; i >= 0; i -= 1 { | |
| s := &game.snakes[i] | |
| if update_visuals(s, &game.foods) { | |
| destroy_snake(s) | |
| unordered_remove(&game.snakes, i) | |
| continue | |
| } | |
| think(s, occupied, game.foods) | |
| move_snake(s, occupied, &game.foods) | |
| } | |
| if len(game.foods) < BASE_FOOD / 2 { | |
| append(&game.foods, Point{ rand.int_max(GRID_WIDTH), rand.int_max(GRID_HEIGHT) }) | |
| } | |
| if alive_count == 0 && len(game.snakes) == 0 { | |
| restart_game(game) | |
| } | |
| raylib.BeginDrawing() | |
| raylib.ClearBackground(raylib.Color{ 20, 20, 25, 255 }) | |
| ox := i32(game.camera_pos.x) + 50 | |
| oy := i32(game.camera_pos.y) + 50 | |
| fw := i32(GRID_WIDTH * CELL_SIZE) | |
| fh := i32(GRID_HEIGHT * CELL_SIZE) | |
| raylib.DrawRectangle(ox, oy, fw, fh, raylib.Color{ 30, 30, 35, 255 }) | |
| raylib.DrawRectangleLines(ox, oy, fw, fh, raylib.GRAY) | |
| time := raylib.GetTime() | |
| pulse := 1.0 + math.sin(time * 5.0) * 0.1 | |
| for f in game.foods { | |
| size := i32(f32(CELL_SIZE) * f32(pulse)) | |
| offset := (i32(CELL_SIZE) - size) / 2 | |
| raylib.DrawRectangle( | |
| ox + i32(f.x) * CELL_SIZE + offset, | |
| oy + i32(f.y) * CELL_SIZE + offset, | |
| size, size, raylib.RED) | |
| } | |
| for &s in game.snakes { | |
| draw_size := i32(f32(CELL_SIZE) * s.scale) | |
| offset := (i32(CELL_SIZE) - draw_size) / 2 | |
| col := s.color | |
| if s.state == .DyingShrink { | |
| col = raylib.Fade(col, s.scale) | |
| } else if s.state == .DyingRot { | |
| col = raylib.WHITE | |
| } | |
| for p, idx in s.body { | |
| px := ox + i32(p.x) * CELL_SIZE + offset | |
| py := oy + i32(p.y) * CELL_SIZE + offset | |
| final_col := col | |
| if idx == 0 && s.state == .Alive { | |
| final_col = raylib.WHITE | |
| } | |
| raylib.DrawRectangle(px, py, draw_size, draw_size, final_col) | |
| } | |
| } | |
| mode_str := game.mode == 1 ? "ELITE (Top 3)" : "CHAOS (Random)" | |
| raylib.DrawText(fmt.ctprintf("MODE: %s", mode_str), 20, 20, 30, raylib.GOLD) | |
| raylib.DrawText("Press '1', '2' or 'R'", 20, 60, 20, raylib.GRAY) | |
| raylib.DrawText("Press 'S' to Save", 20, 85, 20, raylib.GRAY) | |
| raylib.DrawText(fmt.ctprintf("Alive: %d", alive_count), 20, 110, 20, raylib.WHITE) | |
| raylib.DrawFPS(SCREEN_W - 100, 10) | |
| if game.msg_timer > 0 { | |
| raylib.DrawText("SAVED!", SCREEN_W / 2 - 50, SCREEN_H / 2, 40, raylib.GREEN) | |
| } | |
| raylib.EndDrawing() | |
| free_all(context.temp_allocator) | |
| } | |
| for &s in game.snakes { | |
| destroy_snake(&s) | |
| } | |
| delete(game.snakes) | |
| delete(game.foods) | |
| for g in game.population { | |
| free_genome(g) | |
| free(g) | |
| } | |
| delete(game.population) | |
| free(game) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment