diff --git a/ant_viewer.html b/ant_viewer.html index a70dea3..cbfdc21 100644 --- a/ant_viewer.html +++ b/ant_viewer.html @@ -106,11 +106,24 @@ ctx.shadowBlur = 0; } + // 5. FORMICHE (Colorate in base alla velocità) if (data.ants) { - ctx.fillStyle = "#ff8800"; data.ants.forEach(ant => { + // ant[0] = x, ant[1] = y, ant[2] = speed const ax = ant[0] * cellSize; const ay = ant[1] * cellSize; + const speed = ant[2]; + + // Interpolazione Colore + // Speed 0.5 (Lenta) -> Blu (0, 0, 255) + // Speed 1.0 (Media) -> Arancio (255, 165, 0) + // Speed 2.5 (Veloce) -> Rosso Fuoco (255, 0, 0) + + let color = "orange"; + if (speed > 1.3) color = `rgb(255, ${255 - (speed*80)}, 0)`; // Diventa rossa + else if (speed < 0.9) color = `rgb(${speed*100}, ${speed*100}, 255)`; // Diventa blu + + ctx.fillStyle = color; if (cellSize < 3) { ctx.fillRect(ax, ay, cellSize, cellSize); diff --git a/src/env.zig b/src/env.zig index e426851..add1a29 100644 --- a/src/env.zig +++ b/src/env.zig @@ -12,7 +12,14 @@ pub const Ant = struct { x: usize, y: usize, alive: bool, - steps: usize, + energy: f32, // Sostituisce 'steps'. Parte da 100, scende a 0. + + // GENI EVOLUTIVI + gene_speed: f32, // 0.8 (Lenta) -> 2.5 (Schizzata) + gene_metabolism: f32, // 0.5 (Efficiente) -> 2.0 (Sprecona) + + generation: usize, // Per vedere l'evoluzione + score: usize, // Quanti cibi ha mangiato in vita }; pub const World = struct { @@ -21,7 +28,8 @@ pub const World = struct { ants: [NUM_ANTS]Ant, food_x: usize, food_y: usize, - max_steps: usize, + + best_ant_idx: usize, // L'indice della formica "Regina" (miglior genitore) prng: std.Random.DefaultPrng, pub fn init(seed: u64) World { @@ -31,7 +39,7 @@ pub const World = struct { .ants = undefined, .food_x = 0, .food_y = 0, - .max_steps = 1000, + .best_ant_idx = 0, .prng = std.Random.DefaultPrng.init(seed), }; w.reset(); @@ -39,6 +47,9 @@ pub const World = struct { } pub fn reset(self: *World) void { + const random = self.prng.random(); + + // 1. Reset Mappa for (0..GRID_SIZE) |y| { for (0..GRID_SIZE) |x| { self.pheromones[y][x] = 0.0; @@ -49,16 +60,21 @@ pub const World = struct { } } } - self.respawnFood(); + // 2. Spawn Formiche (Adamo ed Eva: Geni casuali standard) const center = GRID_SIZE / 2; for (0..NUM_ANTS) |i| { self.ants[i] = Ant{ .x = center, .y = center, .alive = true, - .steps = 0, + .energy = 100.0, + // Geni iniziali random ma bilanciati + .gene_speed = 0.8 + random.float(f32) * 0.4, // 0.8 - 1.2 + .gene_metabolism = 1.0, + .generation = 0, + .score = 0, }; } } @@ -90,88 +106,163 @@ pub const World = struct { return false; } + // --- LOGICA EVOLUTIVA --- + // Crea una nuova formica clonando i geni del genitore + mutazione + pub fn spawnChild(self: *World, parent_idx: usize) Ant { + const parent = self.ants[parent_idx]; + const random = self.prng.random(); + + // Mutazione: +/- 10% + var new_speed = parent.gene_speed + (random.float(f32) * 0.2 - 0.1); + + // Limiti biologici (nessuna formica supersonica o immobile) + if (new_speed < 0.5) new_speed = 0.5; + if (new_speed > 3.0) new_speed = 3.0; + + // Metabolismo: Chi è veloce brucia esponenzialmente di più + // Formula: Costo = Speed^2 + const new_metabolism = new_speed * new_speed; + + return Ant{ + .x = GRID_SIZE / 2, + .y = GRID_SIZE / 2, + .alive = true, + .energy = 100.0, // Energia piena alla nascita + .gene_speed = new_speed, + .gene_metabolism = new_metabolism, + .generation = parent.generation + 1, + .score = 0, + }; + } + + // Aggiorniamo chi è il genitore migliore + pub fn updateBestAnt(self: *World) void { + var max_score: usize = 0; + var best_idx: usize = 0; + + for (self.ants, 0..) |ant, i| { + if (ant.alive and ant.score > max_score) { + max_score = ant.score; + best_idx = i; + } + } + // Se qualcuno ha fatto punti, diventa il nuovo standard + if (max_score > 0) { + self.best_ant_idx = best_idx; + } + } + pub fn stepAnt(self: *World, ant_idx: usize, action: usize) struct { f32, bool } { var ant = &self.ants[ant_idx]; if (!ant.alive) return .{ 0.0, true }; - ant.steps += 1; + const random = self.prng.random(); - const old_dist_x = if (ant.x > self.food_x) ant.x - self.food_x else self.food_x - ant.x; - const old_dist_y = if (ant.y > self.food_y) ant.y - self.food_y else self.food_y - ant.y; - const old_dist = old_dist_x + old_dist_y; + // 1. GESTIONE VELOCITÀ (Azione Probabilistica) + // Se speed < 1.0, a volte salta il turno. + // Se speed > 1.0, a volte fa un doppio turno (ma qui semplifichiamo: fa sempre 1 turno ma costa diverso) + // Facciamo così: Speed determina QUANTI passi fa in un loop interno. - var new_x = ant.x; - var new_y = ant.y; + var moves_to_make: usize = 0; + var chance = ant.gene_speed; - if (action == 0) new_y -= 1; - if (action == 1) new_y += 1; - if (action == 2) new_x -= 1; - if (action == 3) new_x += 1; - - if (self.grid[new_y][new_x] == TILE_WALL) return .{ -5.0, false }; - - if (self.isOccupied(new_x, new_y)) { - if (old_dist > 5) return .{ -0.5, false }; - } - - const dist_x = if (new_x > self.food_x) new_x - self.food_x else self.food_x - new_x; - const dist_y = if (new_y > self.food_y) new_y - self.food_y else self.food_y - new_y; - const new_dist = dist_x + dist_y; - - var reward: f32 = -0.1; - - const scent_intensity = self.getScent(new_x, new_y); - - const pheromone_level = self.pheromones[new_y][new_x]; - - if (scent_intensity > 0.15) { - reward += 0.1; - } else { - if (pheromone_level > 0.1) { - reward -= 0.5 * pheromone_level; + // Esempio: Speed 2.3 -> Fa sicuri 2 passi, e 30% probabilità del 3° + while (chance > 0) { + if (chance >= 1.0) { + moves_to_make += 1; + chance -= 1.0; } else { - reward += 0.2; + if (random.float(f32) < chance) moves_to_make += 1; + chance = 0; } } - if (new_dist < old_dist) { - reward += 1.5 + (scent_intensity * 2.0); - } else if (new_dist > old_dist) { - reward -= 1.0; + // Se è troppo lenta e salta il turno, perde comunque un po' di energia vitale + if (moves_to_make == 0) { + ant.energy -= 0.1; + if (ant.energy <= 0) return .{ -10.0, true }; + return .{ 0.0, false }; } - ant.x = new_x; - ant.y = new_y; + var total_reward: f32 = 0.0; - self.pheromones[new_y][new_x] = 1.0; + // Loop di movimento (Speed) + for (0..moves_to_make) |_| { + // Se muore durante i passi multipli, stop + if (ant.energy <= 0) break; - if (new_x == self.food_x and new_y == self.food_y) { - self.respawnFood(); - ant.steps = 0; - return .{ 100.0, false }; + // CONSUMO ENERGIA BASATO SUL METABOLISMO + ant.energy -= (0.2 * ant.gene_metabolism); + + // Logica movimento classica (semplificata per brevità dello snippet) + const old_dist_x = if (ant.x > self.food_x) ant.x - self.food_x else self.food_x - ant.x; + const old_dist_y = if (ant.y > self.food_y) ant.y - self.food_y else self.food_y - ant.y; + const old_dist = old_dist_x + old_dist_y; + + var new_x = ant.x; + var new_y = ant.y; + if (action == 0) new_y -= 1; + if (action == 1) new_y += 1; + if (action == 2) new_x -= 1; + if (action == 3) new_x += 1; + + // Collisioni + if (self.grid[new_y][new_x] == TILE_WALL or self.isOccupied(new_x, new_y)) { + // Sbatte: perde energia e ferma il movimento multiplo + ant.energy -= 1.0; + total_reward -= 0.5; + break; + } + + // Movimento valido + ant.x = new_x; + ant.y = new_y; + self.pheromones[new_y][new_x] = 1.0; + + // Olfatto Reward + const new_dist = if (new_x > self.food_x) new_x - self.food_x else self.food_x - new_x; + const total_new_dist = new_dist + (if (new_y > self.food_y) new_y - self.food_y else self.food_y - new_y); + + const scent = self.getScent(new_x, new_y); + if (total_new_dist < old_dist) total_reward += 1.5 + scent; + if (total_new_dist > old_dist) total_reward -= 1.0; + + // Mangia? + if (new_x == self.food_x and new_y == self.food_y) { + self.respawnFood(); + // RICARICA ENERGIA! + ant.energy = 100.0; // Pancia piena + ant.score += 1; + total_reward += 50.0; + + // Aggiorna chi è il migliore + self.updateBestAnt(); + } } - if (ant.steps >= self.max_steps) { - ant.x = GRID_SIZE / 2; - ant.y = GRID_SIZE / 2; - ant.steps = 0; - return .{ -10.0, false }; + // Morte + if (ant.energy <= 0) { + // RESPAWN EVOLUTIVO + // Rinasce come figlia della migliore formica attuale + self.ants[ant_idx] = self.spawnChild(self.best_ant_idx); + return .{ -10.0, true }; } - return .{ reward, false }; + return .{ total_reward, false }; } pub fn evaporatePheromones(self: *World) void { for (0..GRID_SIZE) |y| { for (0..GRID_SIZE) |x| { - if (self.pheromones[y][x] > 0) { - self.pheromones[y][x] *= 0.995; - } + if (self.pheromones[y][x] > 0) self.pheromones[y][x] *= 0.995; } } } pub fn getAntObservation(self: *World, allocator: Allocator, ant_idx: usize) ![]f32 { + // Stessa logica di prima (15 input), copiato dal messaggio precedente + // Per brevità non lo riscrivo tutto, usa quello con i sensori binari + // IMPORTANTE: Assicurati di avere la versione con 15 input qui! var obs = try allocator.alloc(f32, 15); const ant = self.ants[ant_idx]; var idx: usize = 0; @@ -185,7 +276,6 @@ pub const World = struct { while (dx <= 1) : (dx += 1) { const py = @as(usize, @intCast(ay + dy)); const px = @as(usize, @intCast(ax + dx)); - var val: f32 = 0.0; if (self.grid[py][px] == TILE_WALL) { val = -1.0; @@ -198,15 +288,12 @@ pub const World = struct { idx += 1; } } - obs[9] = self.getScent(ant.x, ant.y); obs[10] = self.pheromones[ant.y][ant.x]; - obs[11] = if (self.food_y < ant.y) 1.0 else 0.0; obs[12] = if (self.food_y > ant.y) 1.0 else 0.0; obs[13] = if (self.food_x < ant.x) 1.0 else 0.0; obs[14] = if (self.food_x > ant.x) 1.0 else 0.0; - return obs; } }; diff --git a/src/main.zig b/src/main.zig index 36a7f42..cfbbead 100644 --- a/src/main.zig +++ b/src/main.zig @@ -29,7 +29,7 @@ fn argmax(slice: []const f32) usize { return idx; } -fn exportAntJSON(world: *World, file_path: []const u8, episode: usize, epsilon: f32) !void { +fn exportAntJSON(world: *World, file_path: []const u8, episode: usize, avg_epsilon: f32) !void { const file = try std.fs.cwd().createFile(file_path, .{}); defer file.close(); @@ -37,18 +37,19 @@ fn exportAntJSON(world: *World, file_path: []const u8, episode: usize, epsilon: var fba = std.heap.FixedBufferAllocator.init(&buffer); const allocator = fba.allocator(); + // JSON Formiche COMPLESSO: [[x, y, speed_gene], ...] var ants_json = std.ArrayList(u8){}; defer ants_json.deinit(allocator); try ants_json.appendSlice(allocator, "["); for (world.ants, 0..) |ant, i| { if (i > 0) try ants_json.appendSlice(allocator, ","); - try std.fmt.format(ants_json.writer(allocator), "[{d},{d}]", .{ ant.x, ant.y }); + // Passiamo anche la GENETICA (ant.gene_speed) + try std.fmt.format(ants_json.writer(allocator), "[{d},{d},{d:.2}]", .{ ant.x, ant.y, ant.gene_speed }); } try ants_json.appendSlice(allocator, "]"); - const json = try std.fmt.allocPrint(allocator, "{{\n \"grid_size\": {d},\n \"food\": [{d}, {d}],\n \"ants\": {s},\n \"episode\": {d},\n \"epsilon\": {d:.3}\n}}", .{ env.GRID_SIZE, world.food_x, world.food_y, ants_json.items, episode, epsilon }); - + const json = try std.fmt.allocPrint(allocator, "{{\n \"grid_size\": {d},\n \"food\": [{d}, {d}],\n \"ants\": {s},\n \"episode\": {d},\n \"epsilon\": {d:.3}\n}}", .{ env.GRID_SIZE, world.food_x, world.food_y, ants_json.items, episode, avg_epsilon }); try file.writeAll(json); }