Pulizia codice

This commit is contained in:
Riccardo Forese 2026-02-03 14:55:50 +01:00
parent 9653eada03
commit ed8080e2d6
4 changed files with 27 additions and 97 deletions

View file

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

View file

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

View file

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

View file

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