Le formiche riescono ad imparare a raggiungere il cibo
This commit is contained in:
parent
aeb830bb0e
commit
c3a22bb1a8
|
|
@ -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);
|
||||
|
|
|
|||
211
src/env.zig
211
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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue