first commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
arachne
|
||||
|
||||
# C-Flat ignores
|
||||
cflat
|
||||
cflat-*.txt
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
12
Makefile
Normal 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
39
README.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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
BIN
preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
99
src/NervousSystem.hpp
Normal file
99
src/NervousSystem.hpp
Normal 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
147
src/Spider.hpp
Normal 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
49
src/World.hpp
Normal 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
81
src/main.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user