Le formiche riescono ad imparare a raggiungere il cibo

This commit is contained in:
Riccardo Forese 2026-02-03 15:17:14 +01:00
parent aeb830bb0e
commit c3a22bb1a8
3 changed files with 168 additions and 67 deletions

View file

@ -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);

View file

@ -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;
}
};

View file

@ -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);
}