first commit

This commit is contained in:
David Ali
2026-02-04 13:05:01 +01:00
commit 9299ada186
9 changed files with 453 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
arachne
# C-Flat ignores
cflat
cflat-*.txt

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dávid Ali
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

12
Makefile Normal file
View File

@@ -0,0 +1,12 @@
CXX = g++
CXXFLAGS = -std=c++17 -O3 -Wall
LDFLAGS = -lraylib
SRC = src/main.cpp
TARGET = arachne
all:
$(CXX) $(SRC) -o $(TARGET) $(CXXFLAGS) $(LDFLAGS)
clean:
rm -f $(TARGET)

39
README.md Normal file
View File

@@ -0,0 +1,39 @@
# Arachne: An Experiment in Digital Instinct
**Arachne**ˈræk.ni/ brings a digital creature to life. It is not a standard game character: it is a biological simulation driven by a **Spiking Neural Network (SNN)**. This project explores the boundary between code and instinct. It demonstrates how raw sensory data transforms into movement through a web of artificial neurons.
![Simulation View](preview.png)
## The Anatomy of a Hunter
The simulation centers on a spider called Arachne. It does not follow scripted paths. It hunts using a sophisticated nervous system. The creature possesses a multi-faceted visual system: different "eyes" cover specific angles and ranges to detect prey.
Vision is not its only sense. The spider feels the world through its legs. Specialized vibration sensors detect the movement of flies nearby. These sensory inputs flow directly into the neural network, they stimulate specific neurons based on proximity and angle. The output is not just data, it is an action. Motor units fire to rotate the body, move forward, or freeze in response to stimuli.
## The Neuromorphic Brain
The core of this project is the **NervousSystem** class. It mimics biological processing using a **Leaky Integrate-and-Fire (LIF)** model. Neurons in this system do not pass continuous numbers like deep learning models. Instead: they accumulate an electric charge over time.
This charge decays if no input is received: this mimics biological recovery. When the potential hits a critical threshold: the neuron "spikes". It fires a single discrete signal to its neighbors. Excitatory synapses increase the charge of target neurons, while inhibitory synapses suppress them. This creates a complex dynamic where behavior emerges from the timing of these spikes.
## Simulation vs. Reality
This project pushes the limits of standard computing to mimic biology. However, it remains an approximation. A true biological brain operates asynchronously in continuous time. Arachne currently lives within a synchronous loop: its nervous system updates in lockstep with the 60 FPS frame rate.
The current network is also static. The synaptic weights are hardcoded to define behavior: the spider is born with its instincts already formed. It does not yet learn from its failures.
## The Path Forward
The evolution of Arachne is just beginning. The next major step is to break free from the frame rate. I plan to implement an **Event-Based Neural Engine**. This will allow neurons to fire asynchronously via a priority queue, which will create true time independence.
I also aim to introduce plasticity. By implementing **STDP (Spike-Timing-Dependent Plasticity)** the spider will gain the ability to learn. It will strengthen connections that lead to a successful catch. The simulation will eventually support complex morphology and interaction with dangerous environments.
## Technical Foundation
* **Language**: C++17
* **Engine**: raylib
* **Structure**: Simple Makefile system
## License
MIT

BIN
preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

99
src/NervousSystem.hpp Normal file
View File

@@ -0,0 +1,99 @@
#pragma once
#include <vector>
#include <cstddef>
#include "raylib.h"
#include "raymath.h"
class NervousSystem {
public:
struct Neuron {
private:
friend class NervousSystem;
float potential = 0.0f;
float recovery = 0.92f;
float threshold = 1.0f;
bool fired = false;
bool update(float input) {
potential = (potential * recovery) + input;
if (potential < 0.0f) potential = 0.0f;
if (potential >= threshold) {
potential = 0.0f;
fired = true;
return true;
}
fired = false;
return false;
}
public:
float getCharge() const { return potential / threshold; }
bool hasFired() const { return fired; }
};
private:
struct Synapse {
int from;
int to;
float weight;
};
std::vector<Neuron> units;
std::vector<Synapse> connections;
std::vector<float> incomingSignals;
public:
int addUnit() {
units.push_back(Neuron());
incomingSignals.push_back(0.0f);
return static_cast<int>(units.size()) - 1;
}
void connect(int fromIdx, int toIdx, float weight) {
if (fromIdx < (int)units.size() && toIdx < (int)units.size()) {
connections.push_back({fromIdx, toIdx, weight});
}
}
void tick() {
for (std::size_t i = 0; i < units.size(); ++i) {
units[i].update(incomingSignals[i]);
incomingSignals[i] = 0.0f;
}
for (const auto& syn : connections) {
if (units[syn.from].fired) {
incomingSignals[syn.to] += syn.weight;
}
}
}
void stimulate(int idx, float power) {
if (idx >= 0 && idx < (int)incomingSignals.size()) {
incomingSignals[idx] += power;
}
}
void draw(Vector2 pos, int rows, float spacing) const {
for (const auto& syn : connections) {
Vector2 p1 = {pos.x + (syn.from / rows) * spacing * 2, pos.y + (syn.from % rows) * spacing};
Vector2 p2 = {pos.x + (syn.to / rows) * spacing * 2, pos.y + (syn.to % rows) * spacing};
Color synColor = units[syn.from].fired ? YELLOW : Fade(GRAY, 0.3f);
DrawLineEx(p1, p2, 1.0f, synColor);
}
for (std::size_t i = 0; i < units.size(); ++i) {
Vector2 nodePos = {pos.x + (i / rows) * spacing * 2, pos.y + (i % rows) * spacing};
float charge = units[i].getCharge();
Color nodeColor = ColorLerp(DARKGRAY, LIME, charge);
if (units[i].fired) nodeColor = WHITE;
DrawCircleV(nodePos, 8, nodeColor);
DrawCircleLinesV(nodePos, 8, units[i].fired ? YELLOW : DARKGRAY);
}
}
const Neuron& getUnit(int idx) const { return units[idx]; }
};

147
src/Spider.hpp Normal file
View File

@@ -0,0 +1,147 @@
#pragma once
#include "raylib.h"
#include "raymath.h"
#include "NervousSystem.hpp"
#include <vector>
#include <cmath>
struct Eye {
int unitIdx;
float angleOffset;
float fov;
float range;
};
class Spider {
public:
NervousSystem brain;
Vector2 position;
float angle;
std::vector<Eye> eyes;
std::vector<int> vibrationUnits;
std::vector<int> motorUnits;
int freezeUnit;
Spider(Vector2 startPos) : position(startPos), angle(0.0f) {
auto addEye = [&](float offset, float fov, float range) {
int id = brain.addUnit();
eyes.push_back({id, offset, fov, range});
};
addEye(-5.0f, 30.0f, 600.0f);
addEye(5.0f, 30.0f, 600.0f);
addEye(-20.0f, 60.0f, 500.0f);
addEye(20.0f, 60.0f, 500.0f);
addEye(-45.0f, 120.0f, 350.0f);
addEye(45.0f, 120.0f, 350.0f);
addEye(-60.0f, 180.0f, 200.0f);
addEye(60.0f, 180.0f, 200.0f);
for (int i = 0; i < 8; i++) {
vibrationUnits.push_back(brain.addUnit());
}
for (int i = 0; i < 8; i++) {
motorUnits.push_back(brain.addUnit());
}
freezeUnit = brain.addUnit();
for (int m : motorUnits) {
brain.connect(freezeUnit, m, -5.0f);
}
for (int i = 0; i < 8; i++) {
for (int m = 0; m < 8; m++) {
if ((i < 4 && m >= 4) || (i >= 4 && m < 4)) {
brain.connect(vibrationUnits[i], motorUnits[m], 2.2f);
}
if (i == m) {
brain.connect(vibrationUnits[i], motorUnits[m], 0.4f);
}
}
}
for (int e = 0; e < 8; e++) {
float eyeAng = eyes[e].angleOffset;
float focusMultiplier = (std::abs(eyeAng) <= 30.0f) ? 2.8f : 1.5f;
int targetLegIdx = -1;
if (eyeAng < -15.0f) {
targetLegIdx = 7;
} else if (eyeAng > 15.0f) {
targetLegIdx = 0;
} else if (std::abs(eyeAng) <= 15.0f) {
brain.connect(eyes[e].unitIdx, motorUnits[0], focusMultiplier * 0.8f);
brain.connect(eyes[e].unitIdx, motorUnits[7], focusMultiplier * 0.8f);
}
if (targetLegIdx != -1) {
brain.connect(eyes[e].unitIdx, motorUnits[targetLegIdx], focusMultiplier);
}
}
}
void update() {
brain.tick();
float turnStep = 5.0f * DEG2RAD;
float moveStep = 3.0f;
for (int i = 0; i < 8; i++) {
if (brain.getUnit(motorUnits[i]).hasFired()) {
if (i < 4)
angle += turnStep;
else
angle -= turnStep;
Vector2 forward = {cosf(angle), sinf(angle)};
position = Vector2Add(position, Vector2Scale(forward, moveStep));
}
}
}
void draw() {
DrawCircleV(position, 15, (Color){30, 30, 30, 255});
Vector2 headPos = Vector2Add(position, Vector2Rotate({12, 0}, angle));
DrawCircleV(headPos, 10, BLACK);
for (const auto& eye : eyes) {
float totalAngle = angle * RAD2DEG + eye.angleOffset;
DrawCircleSector(headPos, eye.range, totalAngle - eye.fov / 2, totalAngle + eye.fov / 2, 10, Fade(RED, 0.1f));
DrawCircleSectorLines(headPos, eye.range, totalAngle - eye.fov / 2, totalAngle + eye.fov / 2, 10,
Fade(RED, 0.2f));
}
for (int side : {-1, 1}) {
Vector2 eyePos = Vector2Add(headPos, Vector2Rotate({6, (float)side * 4}, angle));
DrawCircleV(eyePos, 2, RED);
}
for (int i = 0; i < 8; i++) {
float legOffset = (i < 4) ? (-135.0f + i * 30.0f) : (45.0f + (i - 4) * 30.0f);
float legAngle = angle + legOffset * DEG2RAD;
const auto& unit = brain.getUnit(motorUnits[i]);
float length = 35.0f + (unit.hasFired() ? 15.0f : unit.getCharge() * 5.0f);
Vector2 legEnd = Vector2Add(position, Vector2Rotate({length, 0}, legAngle));
Color legColor = ColorLerp(BLACK, RED, unit.getCharge());
if (unit.hasFired()) legColor = MAROON;
DrawLineEx(position, legEnd, 2.0f, legColor);
DrawCircleV(legEnd, 3, legColor);
}
if (brain.getUnit(freezeUnit).hasFired()) {
DrawText("!!! STRACH !!!", position.x - 40, position.y - 40, 10, RED);
}
}
};

49
src/World.hpp Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
#include "raylib.h"
#include <vector>
struct Fly {
Vector2 position;
Vector2 velocity;
bool isAlive = true;
};
class World {
public:
std::vector<Fly> flies;
World(int count, int w, int h) {
for (int i = 0; i < count; i++) {
flies.push_back({{(float)GetRandomValue(0, w), (float)GetRandomValue(0, h)},
{(float)GetRandomValue(-100, 100) / 40.0f, (float)GetRandomValue(-100, 100) / 40.0f}});
}
}
void update() {
for (auto& f : flies) {
if (!f.isAlive) continue;
f.position = Vector2Add(f.position, f.velocity);
if (f.position.x < 0 || f.position.x > 1200) f.velocity.x *= -1;
if (f.position.y < 0 || f.position.y > 800) f.velocity.y *= -1;
}
}
void draw() {
for (const auto& f : flies) {
if (f.isAlive) DrawCircleV(f.position, 3, LIME);
}
}
void respawn(int targetCount) {
int currentAlive = 0;
for (const auto& f : flies) {
if (f.isAlive) currentAlive++;
}
if (currentAlive < targetCount) {
if (rand() % 100 == 0)
flies.push_back({{(float)GetRandomValue(0, 1200), (float)GetRandomValue(0, 800)},
{(float)GetRandomValue(-100, 100) / 40.0f, (float)GetRandomValue(-100, 100) / 40.0f},
true});
}
}
};

81
src/main.cpp Normal file
View File

@@ -0,0 +1,81 @@
#include "raylib.h"
#include "raymath.h"
#include "Spider.hpp"
#include "World.hpp"
#include <cmath>
int main() {
const int screenWidth = 1200;
const int screenHeight = 800;
InitWindow(screenWidth, screenHeight, "Arachne");
SetTargetFPS(60);
Spider arachne({(float)screenWidth / 2, (float)screenHeight / 2});
World world(15, screenWidth, screenHeight);
while (!WindowShouldClose()) {
world.update();
world.respawn(15);
for (auto& fly : world.flies) {
if (!fly.isAlive) continue;
Vector2 toFly = Vector2Subtract(fly.position, arachne.position);
float dist = Vector2Length(toFly);
if (dist < 20.0f) {
fly.isAlive = false;
continue;
}
for (const auto& eye : arachne.eyes) {
if (dist < eye.range) {
float absEyeAng = arachne.angle + eye.angleOffset * DEG2RAD;
Vector2 viewDir = {cosf(absEyeAng), sinf(absEyeAng)};
Vector2 toFlyNorm = Vector2Normalize(toFly);
float dot = Vector2DotProduct(toFlyNorm, viewDir);
float angleToFly = acosf(fmaxf(-1.0f, fminf(1.0f, dot))) * RAD2DEG;
if (angleToFly < eye.fov / 2.0f) {
float intensity = (1.0f - dist / eye.range) * 0.5f;
arachne.brain.stimulate(eye.unitIdx, intensity);
}
}
}
}
for (const auto& fly : world.flies) {
if (!fly.isAlive) continue;
for (int i = 0; i < 8; i++) {
float legOff = (i < 4) ? (-135.0f + i * 30.0f) : (45.0f + (i - 4) * 30.0f);
Vector2 legPos = Vector2Add(arachne.position, Vector2Rotate({40, 0}, arachne.angle + legOff * DEG2RAD));
float distToLeg = Vector2Distance(fly.position, legPos);
if (distToLeg < 150.0f) {
float vibPower = 0.3f * (1.0f - distToLeg / 150.0f);
arachne.brain.stimulate(arachne.vibrationUnits[i], vibPower);
}
}
}
arachne.update();
BeginDrawing();
ClearBackground((Color){30, 30, 40, 255});
arachne.draw();
DrawText("NEURAL NETWORK ACTIVITY", 950, 20, 10, GRAY);
arachne.brain.draw({1000, 50}, 8, 30.0f);
world.draw();
DrawFPS(10, 10);
EndDrawing();
}
CloseWindow();
return 0;
}