diff --git a/src/env.zig b/src/env.zig index 9f5b42a..e426851 100644 --- a/src/env.zig +++ b/src/env.zig @@ -1,16 +1,13 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -pub const GRID_SIZE = 100; // Mappa Gigante! -pub const NUM_ANTS = 20; // La Colonia +pub const GRID_SIZE = 100; +pub const NUM_ANTS = 20; pub const TILE_EMPTY = 0; pub const TILE_WALL = 1; pub const TILE_FOOD = 2; -// Non c'è più TILE_ANT nella griglia statica, perché le formiche si muovono sopra -// Useremo una lista dinamica. - pub const Ant = struct { x: usize, y: usize, @@ -20,8 +17,8 @@ pub const Ant = struct { pub const World = struct { grid: [GRID_SIZE][GRID_SIZE]u8, - pheromones: [GRID_SIZE][GRID_SIZE]f32, // 0.0 = Neutro, 1.0 = Appena visitato - ants: [NUM_ANTS]Ant, // Array fisso di formiche + pheromones: [GRID_SIZE][GRID_SIZE]f32, + ants: [NUM_ANTS]Ant, food_x: usize, food_y: usize, max_steps: usize, @@ -34,7 +31,7 @@ pub const World = struct { .ants = undefined, .food_x = 0, .food_y = 0, - .max_steps = 1000, // Più passi per mappa grande + .max_steps = 1000, .prng = std.Random.DefaultPrng.init(seed), }; w.reset(); @@ -42,8 +39,6 @@ pub const World = struct { } pub fn reset(self: *World) void { - //const random = self.prng.random(); - for (0..GRID_SIZE) |y| { for (0..GRID_SIZE) |x| { self.pheromones[y][x] = 0.0; @@ -55,10 +50,8 @@ pub const World = struct { } } - // 2. Spawn Cibo self.respawnFood(); - // 3. Spawn Formiche (Tutte al centro, come un formicaio) const center = GRID_SIZE / 2; for (0..NUM_ANTS) |i| { self.ants[i] = Ant{ @@ -72,7 +65,6 @@ pub const World = struct { fn respawnFood(self: *World) void { const random = self.prng.random(); - // Semplice loop per trovare posto libero while (true) { const rx = random.intRangeAtMost(usize, 1, GRID_SIZE - 2); const ry = random.intRangeAtMost(usize, 1, GRID_SIZE - 2); @@ -84,8 +76,6 @@ pub const World = struct { } } - // Calcola l'olfatto (Distanza inversa dal cibo) - // 0.0 (Lontanissimo) -> 1.0 (Sopra il cibo) fn getScent(self: *World, x: usize, y: usize) f32 { const dx = if (x > self.food_x) x - self.food_x else self.food_x - x; const dy = if (y > self.food_y) y - self.food_y else self.food_y - y; @@ -93,7 +83,6 @@ pub const World = struct { return 1.0 / (dist + 1.0); } - // Controlla se una cella è occupata da UN'ALTRA formica fn isOccupied(self: *World, x: usize, y: usize) bool { for (self.ants) |ant| { if (ant.alive and ant.x == x and ant.y == y) return true; @@ -101,14 +90,12 @@ pub const World = struct { return false; } - // Esegue step per UNA formica specifica 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; - // Salviamo lo stato PRECEDENTE per fare i confronti 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; @@ -116,18 +103,14 @@ pub const World = struct { var new_x = ant.x; var new_y = ant.y; - if (action == 0) new_y -= 1; // UP - if (action == 1) new_y += 1; // DOWN - if (action == 2) new_x -= 1; // LEFT - if (action == 3) new_x += 1; // RIGHT + if (action == 0) new_y -= 1; + if (action == 1) new_y += 1; + if (action == 2) new_x -= 1; + if (action == 3) new_x += 1; - // --- 1. MURI (Limiti Mappa) --- if (self.grid[new_y][new_x] == TILE_WALL) return .{ -5.0, false }; - // --- 2. COLLISIONI TRA FORMICHE --- - // Se c'è traffico, penalità leggera ma non bloccante se c'è cibo vicino if (self.isOccupied(new_x, new_y)) { - // Se siamo vicini al cibo, spingi! Altrimenti aspetta. if (old_dist > 5) return .{ -0.5, false }; } @@ -188,16 +171,14 @@ pub const World = struct { } } - // Input aumentati a 15 pub fn getAntObservation(self: *World, allocator: Allocator, ant_idx: usize) ![]f32 { - var obs = try allocator.alloc(f32, 15); // AUMENTATO A 15 + var obs = try allocator.alloc(f32, 15); const ant = self.ants[ant_idx]; var idx: usize = 0; const ax = @as(i32, @intCast(ant.x)); const ay = @as(i32, @intCast(ant.y)); - // 1. Visione 3x3 (0-8) var dy: i32 = -1; while (dy <= 1) : (dy += 1) { var dx: i32 = -1; @@ -209,27 +190,22 @@ pub const World = struct { if (self.grid[py][px] == TILE_WALL) { val = -1.0; } else if (px == self.food_x and py == self.food_y) { - val = 1.0; // Vedo il cibo vicino! + val = 1.0; } else if (self.isOccupied(px, py) and (dx != 0 or dy != 0)) { - val = -0.5; // Vedo una sorella + val = -0.5; } obs[idx] = val; idx += 1; } } - // 2. Sensi Chimici (9-10) obs[9] = self.getScent(ant.x, ant.y); obs[10] = self.pheromones[ant.y][ant.x]; - // 3. BUSSOLA BINARIA (11-14) - // Risponde alla domanda: "In che quadrante è il cibo?" - // È molto più facile da capire per l'IA rispetto a un float. - - obs[11] = if (self.food_y < ant.y) 1.0 else 0.0; // Cibo è SOPRA? - obs[12] = if (self.food_y > ant.y) 1.0 else 0.0; // Cibo è SOTTO? - obs[13] = if (self.food_x < ant.x) 1.0 else 0.0; // Cibo è SINISTRA? - obs[14] = if (self.food_x > ant.x) 1.0 else 0.0; // Cibo è DESTRA? + 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/layer.zig b/src/layer.zig index 7d76801..2881cb3 100644 --- a/src/layer.zig +++ b/src/layer.zig @@ -10,7 +10,7 @@ pub const DenseLayer = struct { output: []f32, inputs_count: usize, neurons_count: usize, - use_sigmoid: bool, // NUOVO CAMPO + use_sigmoid: bool, allocator: Allocator, fn sigmoid(x: f32) f32 { @@ -21,7 +21,6 @@ pub const DenseLayer = struct { return x * (1.0 - x); } - // Aggiunto parametro 'use_sigmoid' pub fn init(allocator: Allocator, inputs: usize, neurons: usize, seed: u64, use_sigmoid: bool) !DenseLayer { const weights = try allocator.alloc(f32, inputs * neurons); const biases = try allocator.alloc(f32, neurons); @@ -30,7 +29,6 @@ pub const DenseLayer = struct { var prng = std.Random.DefaultPrng.init(seed); const random = prng.random(); - // Pesi più piccoli per stabilità iniziale for (weights) |*w| w.* = (random.float(f32) * 2.0 - 1.0) * 0.1; for (biases) |*b| b.* = 0.0; @@ -56,7 +54,6 @@ pub const DenseLayer = struct { var sum: f32 = self.biases[n]; const w_start = n * self.inputs_count; - // SIMD var vec_sum: Vec = @splat(0.0); var i: usize = 0; while (i + SimdWidth <= self.inputs_count) : (i += SimdWidth) { @@ -66,12 +63,10 @@ pub const DenseLayer = struct { } sum += @reduce(.Add, vec_sum); - // Tail Loop while (i < self.inputs_count) : (i += 1) { sum += input[i] * self.weights[w_start + i]; } - // CORREZIONE: Se non usiamo sigmoide, è lineare (passa 'sum' diretto) if (self.use_sigmoid) { self.output[n] = sigmoid(sum); } else { @@ -86,9 +81,6 @@ pub const DenseLayer = struct { @memset(input_gradient, 0.0); for (0..self.neurons_count) |n| { - // CORREZIONE DERIVATA: - // Se Sigmoide: f'(x) = out * (1 - out) - // Se Lineare: f'(x) = 1.0 var derivative: f32 = 1.0; if (self.use_sigmoid) { derivative = sigmoidDerivative(self.output[n]); @@ -99,7 +91,6 @@ pub const DenseLayer = struct { self.biases[n] -= learning_rate * delta; - // SIMD LOOP (Backprop) const v_delta: Vec = @splat(delta); const v_lr: Vec = @splat(learning_rate); const v_change_factor = v_delta * v_lr; diff --git a/src/main.zig b/src/main.zig index 5ce89a3..9c3b555 100644 --- a/src/main.zig +++ b/src/main.zig @@ -64,14 +64,10 @@ pub fn main() !void { const allocator = gpa.allocator(); defer _ = gpa.deinit(); - // 1. Inizializza Ambiente 100x100 e Rete var world = World.init(12345); var net = Network.init(allocator); defer net.deinit(); - // ARCHITETTURA RETE - // Input 11: (9 Vista + 1 Olfatto + 1 Feromone) - // Output 4: (Su, Giù, Sx, Dx) LINEARE try net.addLayer(15, 40, 111, true); try net.addLayer(40, 4, 222, false); @@ -84,19 +80,13 @@ pub fn main() !void { var global_step: usize = 0; var epsilon: f32 = EPSILON_START; - // Ciclo infinito (o molto lungo) while (true) { - - // 1. Evaporazione Feromoni (Memoria collettiva che sbiadisce) world.evaporatePheromones(); - // 2. Turno di ogni formica for (0..env.NUM_ANTS) |i| { - // A. OSSERVAZIONE const current_obs = try world.getAntObservation(allocator, i); defer allocator.free(current_obs); - // B. SCEGLI AZIONE (Epsilon-Greedy) var action: usize = 0; const q_values = net.forward(current_obs); @@ -106,24 +96,16 @@ pub fn main() !void { action = argmax(q_values); } - // C. ESEGUI AZIONE const result = world.stepAnt(i, action); const reward = result[0]; - // Nota: in multi-agente 'done' è meno rilevante per il loop principale, - // perché se una formica "finisce" (mangia), respawna o continua. - // D. ADDESTRAMENTO (Hive Mind Learning) - // Calcoliamo target Q-Value var target_val = reward; - // Se non è uno stato terminale (morte o vittoria netta), aggiungiamo stima futuro - // Consideriamo la vittoria (mangiare) come continuativa qui per non rompere il flusso const next_obs = try world.getAntObservation(allocator, i); defer allocator.free(next_obs); const next_q_values = net.forward(next_obs); target_val += GAMMA * maxVal(next_q_values); - // Backpropagation var target_vector = try allocator.alloc(f32, 4); defer allocator.free(target_vector); for (0..4) |j| target_vector[j] = q_values[j]; @@ -132,24 +114,19 @@ pub fn main() !void { _ = try net.train(current_obs, target_vector, LR); } - // 3. Gestione Loop Globale global_step += 1; - // Decadimento Epsilon if (epsilon > EPSILON_END) { epsilon -= DECAY_RATE; } - // 4. Export e Log (Ogni 10 step globali per fluidità) if (global_step % 10 == 0) { try exportAntJSON(&world, "ant_state.json", global_step, epsilon); - // Log console ogni 100 step if (global_step % 100 == 0) { std.debug.print("Step: {d} | Epsilon: {d:.3} | Cibo: [{d},{d}]\r", .{ global_step, epsilon, world.food_x, world.food_y }); } - // Pausa per vedere l'animazione (rimuovi per training ultra-veloce) std.Thread.sleep(100 * 1_000_000); } } diff --git a/visualizer.html b/visualizer.html index 3752977..979e836 100644 --- a/visualizer.html +++ b/visualizer.html @@ -9,10 +9,10 @@ background-color: #111; color: #eee; font-family: monospace; - overflow: auto; /* Abilita lo scroll */ + overflow: auto; } #info { - position: fixed; /* Rimane fisso mentre scrolli */ + position: fixed; top: 10px; left: 10px; background: rgba(0,0,0,0.8); @@ -24,8 +24,7 @@ h1 { margin: 0; font-size: 1.2em; color: #f4a261; } .stat { font-size: 0.9em; color: #aaa; margin-top: 5px; } canvas { - display: block; - /* Il canvas non ha dimensioni fisse CSS, le decide JS */ + display: block; } @@ -50,20 +49,16 @@ const epochEl = document.getElementById('epoch'); const lossEl = document.getElementById('loss'); - // Configurazione const MIN_NODE_SPACING = 20; const LAYER_PADDING = 100; - // --- NUOVO: Funzione per disegnare la cifra input --- function drawInputImage(pixels) { if (!pixels || pixels.length === 0) return; - // Disegniamo un box in alto a sinistra - const scale = 4; // Zoom 4x + const scale = 4; const startX = 20; - const startY = 120; // Sotto le scritte di info + const startY = 120; - // Sfondo nero ctx.fillStyle = "#000"; ctx.fillRect(startX - 2, startY - 2, (28 * scale) + 4, (28 * scale) + 4); ctx.strokeStyle = "#f4a261"; @@ -72,18 +67,16 @@ for (let i = 0; i < 784; i++) { const val = pixels[i]; - if (val > 0.1) { // Disegna solo se non è nero + if (val > 0.1) { const x = (i % 28); const y = Math.floor(i / 28); - // Scala di grigi basata sul valore const brightness = Math.floor(val * 255); ctx.fillStyle = `rgb(${brightness}, ${brightness}, ${brightness})`; ctx.fillRect(startX + (x * scale), startY + (y * scale), scale, scale); } } - // Etichetta ctx.fillStyle = "#fff"; ctx.font = "14px monospace"; ctx.fillText("AI INPUT:", startX, startY - 10); @@ -104,13 +97,12 @@ ctx.clearRect(0, 0, canvas.width, canvas.height); } - // --- DISEGNA L'INPUT PRIMA DI TUTTO --- if (data.input_pixels) { drawInputImage(data.input_pixels); } - const layerWidth = (canvas.width - 200) / structure.length; // Lasciamo spazio a sx per l'immagine - const offsetX = 150; // Spostiamo tutto a destra + const layerWidth = (canvas.width - 200) / structure.length; + const offsetX = 150; let nodePositions = []; @@ -129,7 +121,6 @@ nodePositions.push(layerNodes); }); - // Disegno connessioni const DRAW_THRESHOLD = 0.05; layers.forEach((layer, lIdx) => { const sourceNodes = nodePositions[lIdx]; @@ -145,7 +136,7 @@ ctx.moveTo(source.x, source.startY); ctx.lineTo(target.x, target.startY); - const alpha = Math.min(Math.abs(weight), 0.5); // Più trasparente per chiarezza + const alpha = Math.min(Math.abs(weight), 0.5); ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 128, ${alpha})` : `rgba(255, 60, 60, ${alpha})`; ctx.lineWidth = 1; ctx.stroke(); @@ -153,15 +144,12 @@ }); }); - // Disegno Nodi nodePositions.forEach((layerNodes, lIdx) => { const nodeRadius = Math.max(3, Math.min(15, 300 / layerNodes.length)); - - // Evidenziamo l'output attivo nell'ultimo layer! + let maxOutIdx = -1; let maxOutVal = -999; - // Se siamo nell'ultimo layer, cerchiamo il vincitore if (lIdx === nodePositions.length - 1) { // Nota: Non abbiamo i valori di output nel JSON, solo i pesi statici. // Quindi coloriamo solo i nodi genericamente @@ -172,10 +160,8 @@ ctx.arc(node.x, node.startY, nodeRadius, 0, Math.PI * 2); ctx.fillStyle = '#222'; - // Hack visivo: Se siamo nell'ultimo layer, coloriamo i nodi in base all'indice (0-9) if (lIdx === nodePositions.length - 1) { ctx.fillStyle = '#444'; - // Aggiungiamo il numero dentro il nodo ctx.fillText(nIdx, node.x + 20, node.startY + 5); }