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;
|
ctx.shadowBlur = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. FORMICHE (Colorate in base alla velocità)
|
||||||
if (data.ants) {
|
if (data.ants) {
|
||||||
ctx.fillStyle = "#ff8800";
|
|
||||||
data.ants.forEach(ant => {
|
data.ants.forEach(ant => {
|
||||||
|
// ant[0] = x, ant[1] = y, ant[2] = speed
|
||||||
const ax = ant[0] * cellSize;
|
const ax = ant[0] * cellSize;
|
||||||
const ay = ant[1] * 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) {
|
if (cellSize < 3) {
|
||||||
ctx.fillRect(ax, ay, cellSize, cellSize);
|
ctx.fillRect(ax, ay, cellSize, cellSize);
|
||||||
|
|
|
||||||
193
src/env.zig
193
src/env.zig
|
|
@ -12,7 +12,14 @@ pub const Ant = struct {
|
||||||
x: usize,
|
x: usize,
|
||||||
y: usize,
|
y: usize,
|
||||||
alive: bool,
|
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 {
|
pub const World = struct {
|
||||||
|
|
@ -21,7 +28,8 @@ pub const World = struct {
|
||||||
ants: [NUM_ANTS]Ant,
|
ants: [NUM_ANTS]Ant,
|
||||||
food_x: usize,
|
food_x: usize,
|
||||||
food_y: usize,
|
food_y: usize,
|
||||||
max_steps: usize,
|
|
||||||
|
best_ant_idx: usize, // L'indice della formica "Regina" (miglior genitore)
|
||||||
prng: std.Random.DefaultPrng,
|
prng: std.Random.DefaultPrng,
|
||||||
|
|
||||||
pub fn init(seed: u64) World {
|
pub fn init(seed: u64) World {
|
||||||
|
|
@ -31,7 +39,7 @@ pub const World = struct {
|
||||||
.ants = undefined,
|
.ants = undefined,
|
||||||
.food_x = 0,
|
.food_x = 0,
|
||||||
.food_y = 0,
|
.food_y = 0,
|
||||||
.max_steps = 1000,
|
.best_ant_idx = 0,
|
||||||
.prng = std.Random.DefaultPrng.init(seed),
|
.prng = std.Random.DefaultPrng.init(seed),
|
||||||
};
|
};
|
||||||
w.reset();
|
w.reset();
|
||||||
|
|
@ -39,6 +47,9 @@ pub const World = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *World) void {
|
pub fn reset(self: *World) void {
|
||||||
|
const random = self.prng.random();
|
||||||
|
|
||||||
|
// 1. Reset Mappa
|
||||||
for (0..GRID_SIZE) |y| {
|
for (0..GRID_SIZE) |y| {
|
||||||
for (0..GRID_SIZE) |x| {
|
for (0..GRID_SIZE) |x| {
|
||||||
self.pheromones[y][x] = 0.0;
|
self.pheromones[y][x] = 0.0;
|
||||||
|
|
@ -49,16 +60,21 @@ pub const World = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.respawnFood();
|
self.respawnFood();
|
||||||
|
|
||||||
|
// 2. Spawn Formiche (Adamo ed Eva: Geni casuali standard)
|
||||||
const center = GRID_SIZE / 2;
|
const center = GRID_SIZE / 2;
|
||||||
for (0..NUM_ANTS) |i| {
|
for (0..NUM_ANTS) |i| {
|
||||||
self.ants[i] = Ant{
|
self.ants[i] = Ant{
|
||||||
.x = center,
|
.x = center,
|
||||||
.y = center,
|
.y = center,
|
||||||
.alive = true,
|
.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;
|
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 } {
|
pub fn stepAnt(self: *World, ant_idx: usize, action: usize) struct { f32, bool } {
|
||||||
var ant = &self.ants[ant_idx];
|
var ant = &self.ants[ant_idx];
|
||||||
if (!ant.alive) return .{ 0.0, true };
|
if (!ant.alive) return .{ 0.0, true };
|
||||||
|
|
||||||
ant.steps += 1;
|
const random = self.prng.random();
|
||||||
|
|
||||||
|
// 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 moves_to_make: usize = 0;
|
||||||
|
var chance = ant.gene_speed;
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if (random.float(f32) < chance) moves_to_make += 1;
|
||||||
|
chance = 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
var total_reward: f32 = 0.0;
|
||||||
|
|
||||||
|
// Loop di movimento (Speed)
|
||||||
|
for (0..moves_to_make) |_| {
|
||||||
|
// Se muore durante i passi multipli, stop
|
||||||
|
if (ant.energy <= 0) break;
|
||||||
|
|
||||||
|
// 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_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_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;
|
const old_dist = old_dist_x + old_dist_y;
|
||||||
|
|
||||||
var new_x = ant.x;
|
var new_x = ant.x;
|
||||||
var new_y = ant.y;
|
var new_y = ant.y;
|
||||||
|
|
||||||
if (action == 0) new_y -= 1;
|
if (action == 0) new_y -= 1;
|
||||||
if (action == 1) new_y += 1;
|
if (action == 1) new_y += 1;
|
||||||
if (action == 2) new_x -= 1;
|
if (action == 2) new_x -= 1;
|
||||||
if (action == 3) new_x += 1;
|
if (action == 3) new_x += 1;
|
||||||
|
|
||||||
if (self.grid[new_y][new_x] == TILE_WALL) return .{ -5.0, false };
|
// Collisioni
|
||||||
|
if (self.grid[new_y][new_x] == TILE_WALL or self.isOccupied(new_x, new_y)) {
|
||||||
if (self.isOccupied(new_x, new_y)) {
|
// Sbatte: perde energia e ferma il movimento multiplo
|
||||||
if (old_dist > 5) return .{ -0.5, false };
|
ant.energy -= 1.0;
|
||||||
}
|
total_reward -= 0.5;
|
||||||
|
break;
|
||||||
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;
|
|
||||||
} else {
|
|
||||||
reward += 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new_dist < old_dist) {
|
|
||||||
reward += 1.5 + (scent_intensity * 2.0);
|
|
||||||
} else if (new_dist > old_dist) {
|
|
||||||
reward -= 1.0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Movimento valido
|
||||||
ant.x = new_x;
|
ant.x = new_x;
|
||||||
ant.y = new_y;
|
ant.y = new_y;
|
||||||
|
|
||||||
self.pheromones[new_y][new_x] = 1.0;
|
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) {
|
if (new_x == self.food_x and new_y == self.food_y) {
|
||||||
self.respawnFood();
|
self.respawnFood();
|
||||||
ant.steps = 0;
|
// RICARICA ENERGIA!
|
||||||
return .{ 100.0, false };
|
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) {
|
// Morte
|
||||||
ant.x = GRID_SIZE / 2;
|
if (ant.energy <= 0) {
|
||||||
ant.y = GRID_SIZE / 2;
|
// RESPAWN EVOLUTIVO
|
||||||
ant.steps = 0;
|
// Rinasce come figlia della migliore formica attuale
|
||||||
return .{ -10.0, false };
|
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 {
|
pub fn evaporatePheromones(self: *World) void {
|
||||||
for (0..GRID_SIZE) |y| {
|
for (0..GRID_SIZE) |y| {
|
||||||
for (0..GRID_SIZE) |x| {
|
for (0..GRID_SIZE) |x| {
|
||||||
if (self.pheromones[y][x] > 0) {
|
if (self.pheromones[y][x] > 0) self.pheromones[y][x] *= 0.995;
|
||||||
self.pheromones[y][x] *= 0.995;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getAntObservation(self: *World, allocator: Allocator, ant_idx: usize) ![]f32 {
|
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);
|
var obs = try allocator.alloc(f32, 15);
|
||||||
const ant = self.ants[ant_idx];
|
const ant = self.ants[ant_idx];
|
||||||
var idx: usize = 0;
|
var idx: usize = 0;
|
||||||
|
|
@ -185,7 +276,6 @@ pub const World = struct {
|
||||||
while (dx <= 1) : (dx += 1) {
|
while (dx <= 1) : (dx += 1) {
|
||||||
const py = @as(usize, @intCast(ay + dy));
|
const py = @as(usize, @intCast(ay + dy));
|
||||||
const px = @as(usize, @intCast(ax + dx));
|
const px = @as(usize, @intCast(ax + dx));
|
||||||
|
|
||||||
var val: f32 = 0.0;
|
var val: f32 = 0.0;
|
||||||
if (self.grid[py][px] == TILE_WALL) {
|
if (self.grid[py][px] == TILE_WALL) {
|
||||||
val = -1.0;
|
val = -1.0;
|
||||||
|
|
@ -198,15 +288,12 @@ pub const World = struct {
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
obs[9] = self.getScent(ant.x, ant.y);
|
obs[9] = self.getScent(ant.x, ant.y);
|
||||||
obs[10] = self.pheromones[ant.y][ant.x];
|
obs[10] = self.pheromones[ant.y][ant.x];
|
||||||
|
|
||||||
obs[11] = if (self.food_y < ant.y) 1.0 else 0.0;
|
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[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[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;
|
obs[14] = if (self.food_x > ant.x) 1.0 else 0.0;
|
||||||
|
|
||||||
return obs;
|
return obs;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ fn argmax(slice: []const f32) usize {
|
||||||
return idx;
|
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, .{});
|
const file = try std.fs.cwd().createFile(file_path, .{});
|
||||||
defer file.close();
|
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);
|
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||||
const allocator = fba.allocator();
|
const allocator = fba.allocator();
|
||||||
|
|
||||||
|
// JSON Formiche COMPLESSO: [[x, y, speed_gene], ...]
|
||||||
var ants_json = std.ArrayList(u8){};
|
var ants_json = std.ArrayList(u8){};
|
||||||
defer ants_json.deinit(allocator);
|
defer ants_json.deinit(allocator);
|
||||||
try ants_json.appendSlice(allocator, "[");
|
try ants_json.appendSlice(allocator, "[");
|
||||||
|
|
||||||
for (world.ants, 0..) |ant, i| {
|
for (world.ants, 0..) |ant, i| {
|
||||||
if (i > 0) try ants_json.appendSlice(allocator, ",");
|
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, "]");
|
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);
|
try file.writeAll(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue