Skip to content

Commit 11fd169

Browse files
committed
Feature: Overhauled AI brain architecture for improved genetic training and mutation support.
1 parent 011f849 commit 11fd169

27 files changed

+155
-143
lines changed

G33kShell.Desktop/Console/Screensavers/AI/AiBrainBase.cs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ protected AiBrainBase(int inputSize, int[] hiddenLayers, int outputSize)
2323
m_qNet = new NeuralNetwork(inputSize, hiddenLayers, outputSize, learningRate: 0.05);
2424
}
2525

26+
protected AiBrainBase(AiBrainBase toCopy) =>
27+
m_qNet = toCopy.m_qNet.Clone();
28+
2629
protected int ChooseHighestOutput(IAiGameState state) => ArgMax(GetOutputs(state));
2730

2831
protected double[] GetOutputs(IAiGameState state) => m_qNet.Predict(state.ToInputVector());
@@ -49,35 +52,23 @@ private static int ArgMax(double[] values)
4952

5053
public void Load(byte[] brainBytes) => JsonConvert.PopulateObject(brainBytes.DecompressToString(), this);
5154

52-
public AiBrainBase InitWithLerp(AiBrainBase first, AiBrainBase second, double mix)
53-
{
54-
m_qNet = first.m_qNet.CreateLerped(second.m_qNet, mix);
55-
return this;
56-
}
57-
58-
public AiBrainBase InitWithSpliced(AiBrainBase first, AiBrainBase second)
55+
public AiBrainBase Randomize()
5956
{
60-
m_qNet = first.m_qNet.CreateSpliced(second.m_qNet);
57+
m_qNet.Randomize();
6158
return this;
6259
}
6360

64-
public AiBrainBase InitWithNudgedWeights(AiBrainBase brain, NeuralNetwork.NudgeFactor nudge)
61+
public AiBrainBase CrossWith(AiBrainBase second, double crossoverRate)
6562
{
66-
m_qNet = brain.m_qNet.CloneWithNudgeWeights(nudge);
63+
m_qNet.CrossWith(second.m_qNet, crossoverRate);
6764
return this;
6865
}
6966

70-
public AiBrainBase NudgeWeights(NeuralNetwork.NudgeFactor nudge)
67+
public AiBrainBase Mutate(double mutationRate)
7168
{
72-
m_qNet = m_qNet.CloneWithNudgeWeights(nudge);
69+
m_qNet.Mutate(mutationRate);
7370
return this;
7471
}
7572

76-
public AiBrainBase InitWithBrain(AiBrainBase brain)
77-
{
78-
lock (m_qNet)
79-
lock (brain.m_qNet)
80-
m_qNet = brain.m_qNet.Clone();
81-
return this;
82-
}
73+
public abstract AiBrainBase Clone();
8374
}

G33kShell.Desktop/Console/Screensavers/AI/AiGameCanvasBase.cs

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using System.Collections.Generic;
1313
using System.Linq;
1414
using System.Threading.Tasks;
15-
using CSharp.Core.AI;
1615
using CSharp.Core.Extensions;
1716
using G33kShell.Desktop.Console.Controls;
1817
using JetBrains.Annotations;
@@ -24,11 +23,10 @@ namespace G33kShell.Desktop.Console.Screensavers.AI;
2423
/// </summary>
2524
public abstract class AiGameCanvasBase : ScreensaverBase
2625
{
27-
private readonly Random m_rand = new Random();
2826
private int m_generation;
2927
private double m_savedRating;
3028
private const int InitialPopSize = 300;
31-
private const int MinPopSize = 80;
29+
private const int MinPopSize = 150;
3230
private int m_generationsSinceImprovement;
3331
private int m_currentPopSize = InitialPopSize;
3432
private Task m_trainingTask;
@@ -48,7 +46,7 @@ protected AiGameCanvasBase(int width, int height, int targetFps = 30) : base(wid
4846
}
4947

5048
[UsedImplicitly]
51-
protected void TrainAi(ScreenData screen, Action<byte[]> saveBrainBytes, Func<AiBrainBase> createBrain)
49+
protected void TrainAi(ScreenData screen, Action<byte[]> saveBrainBytes)
5250
{
5351
const string animChars = "/-\\|";
5452
var animFrame = Environment.TickCount64 / 100 % animChars.Length;
@@ -64,7 +62,7 @@ protected void TrainAi(ScreenData screen, Action<byte[]> saveBrainBytes, Func<Ai
6462
m_trainingTask = Task.Run(() =>
6563
{
6664
while (!m_stopTraining)
67-
TrainAiImpl(saveBrainBytes, createBrain);
65+
TrainAiImpl(saveBrainBytes);
6866
});
6967
}
7068

@@ -75,7 +73,7 @@ public override void StopScreensaver()
7573
m_stopTraining = true;
7674
}
7775

78-
private void TrainAiImpl(Action<byte[]> saveBrainBytes, Func<AiBrainBase> createBrain)
76+
private void TrainAiImpl(Action<byte[]> saveBrainBytes)
7977
{
8078
m_games ??= Enumerable.Range(0, m_currentPopSize).Select(_ => CreateGameWithSeed(m_generation)).ToArray();
8179

@@ -100,11 +98,14 @@ private void TrainAiImpl(Action<byte[]> saveBrainBytes, Func<AiBrainBase> create
10098
System.Console.WriteLine(stats);
10199

102100
// Persist brain improvements.
101+
AiBrainBase goatBrain = null;
102+
var increaseMutation = false;
103103
if (veryBest.Rating > m_savedRating)
104104
{
105105
m_savedRating = veryBest.Rating;
106106
System.Console.WriteLine("Saved.");
107107
saveBrainBytes(veryBest.Brain.Save());
108+
goatBrain = veryBest.Brain.Clone();
108109

109110
m_generationsSinceImprovement = 0;
110111
}
@@ -113,47 +114,44 @@ private void TrainAiImpl(Action<byte[]> saveBrainBytes, Func<AiBrainBase> create
113114
m_generationsSinceImprovement++;
114115

115116
m_currentPopSize = Math.Max(m_currentPopSize - 2, MinPopSize);
116-
if (m_generationsSinceImprovement >= 100)
117-
{
118-
m_generationsSinceImprovement = 0;
119-
m_currentPopSize = InitialPopSize;
120-
System.Console.WriteLine("Stagnation detected — Perturbing entire population.");
121-
}
117+
// if (m_generationsSinceImprovement >= 100)
118+
// {
119+
// m_generationsSinceImprovement = 0;
120+
// m_currentPopSize = InitialPopSize;
121+
// //increaseMutation = true;
122+
// System.Console.WriteLine("Stagnation detected — Increasing population size.");
123+
// }
122124
}
123125

124126
// Build the brains for the next generation.
125127
var nextBrains = new List<AiBrainBase>(m_games.Length);
126128

129+
// The GOAT lives on.
130+
if (goatBrain != null)
131+
nextBrains.Add(goatBrain.Clone());
132+
127133
// Elite 10% brains get a free pass.
128-
nextBrains.AddRange(eliteGames.Select(o => o.Brain));
134+
nextBrains.AddRange(eliteGames.Select(o => o.Brain.Clone()));
129135

130-
// ...and again with a small variation, (10%)
131-
nextBrains.AddRange(eliteGames.Select(o => createBrain().InitWithNudgedWeights(o.Brain, NeuralNetwork.NudgeFactor.Low)));
136+
// ...and 10% more with a small mutation.
137+
nextBrains.AddRange(eliteGames.Select(o => o.Brain.Clone().Mutate(0.02)));
138+
139+
if (increaseMutation)
140+
{
141+
// Fill 50% of the population with more mutations.
142+
nextBrains.AddRange(orderedGames.Take(orderedGames.Length / 2).Select(o => o.Brain.Clone().Mutate(0.5)));
143+
}
132144

133145
// Spawn 5% pure randoms.
134-
nextBrains.AddRange(Enumerable.Range(0, (int)(m_currentPopSize * 0.05)).Select(_ => createBrain()));
146+
nextBrains.AddRange(Enumerable.Range(0, (int)(m_currentPopSize * 0.05)).Select(_ => veryBest.Brain.Clone().Randomize()));
135147

136148
// Elite get to be parents.
137-
var breeders = eliteGames.Select(o => o.Brain).ToList();
138-
var minBreederCount = Math.Max(5, breeders.Count * 0.25);
139149
while (nextBrains.Count < m_currentPopSize)
140150
{
141-
var mumBrain = breeders[m_rand.Next(breeders.Count)];
142-
var dadBrain = breeders[m_rand.Next(breeders.Count)];
143-
var childBrain = m_rand.Next(0, 4) switch
144-
{
145-
0 => createBrain().InitWithLerp(mumBrain, dadBrain, 0.5),
146-
1 => createBrain().InitWithLerp(mumBrain, dadBrain, m_rand.NextDouble()),
147-
2 => createBrain().InitWithSpliced(mumBrain, dadBrain),
148-
3 => createBrain().InitWithSpliced(mumBrain, dadBrain).NudgeWeights(NeuralNetwork.NudgeFactor.Low),
149-
_ => throw new ArgumentOutOfRangeException()
150-
};
151-
151+
var mumBrain = Random.Shared.RouletteSelection(m_games, o => o.Rating).Brain;
152+
var dadBrain = Random.Shared.RouletteSelection(m_games, o => o.Rating).Brain;
153+
var childBrain = mumBrain.Clone().CrossWith(dadBrain, 0.5).Mutate(0.05);
152154
nextBrains.Add(childBrain);
153-
154-
// Reduce the set size to eliminate the worst breeder.
155-
if (breeders.Count > minBreederCount)
156-
breeders.RemoveAt(breeders.Count - 1);
157155
}
158156

159157
// Make the next generation of games.

G33kShell.Desktop/Console/Screensavers/Asteroids/Asteroid.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,24 @@ public bool Contains(Vector2 pt) =>
101101
/// </summary>
102102
public void Explode(List<Asteroid> asteroids)
103103
{
104-
asteroids.Remove(this);
105-
if (m_size == 0)
104+
if (m_size ==0)
105+
{
106+
asteroids.Remove(this);
106107
return; // This is the smallest an asteroid can be.
108+
}
107109

108110
// Spawn smaller asteroids evenly spaced around this one, moving away from the center.
109-
var countToSpawn = new[] { 2, 3, 4 }[m_size];
111+
var countToSpawn = new[] { 1, 2, 3 }[m_size];
112+
m_size--;
110113
var angleStep = MathF.Tau / countToSpawn;
111114

112115
for (var i = 0; i < countToSpawn; i++)
113116
{
114-
var angle = i * angleStep * m_rand.NextFloat().Lerp(0.8f, 1.2f);
117+
var angle = m_rand.NextFloat() + i * angleStep * m_rand.NextFloat().Lerp(0.8f, 1.2f);
115118
var newPosition = Position + angle.ToDirection() * Radius * 0.5f;
116119
var newAsteroid = new Asteroid(newPosition, m_speed, m_rand, m_arenaWidth, m_arenaHeight, angle)
117120
{
118-
m_size = m_size - 1
121+
m_size = m_size
119122
};
120123

121124
asteroids.Add(newAsteroid);

G33kShell.Desktop/Console/Screensavers/Asteroids/AsteroidsCanvas.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public override void UpdateFrame(ScreenData screen)
3535
screen.ClearChars();
3636

3737
if (ActivationName.Contains("_train"))
38-
TrainAi(screen, brainBytes => Settings.Instance.AsteroidsBrain = brainBytes, () => new Brain());
38+
TrainAi(screen, brainBytes => Settings.Instance.AsteroidsBrain = brainBytes);
3939
else
4040
PlayGame(screen);
4141
}

G33kShell.Desktop/Console/Screensavers/Asteroids/Brain.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99
//
1010
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND.
1111

12-
using G33kShell.Desktop.Console.Screensavers.AI;
12+
using G33kShell.Desktop.Console.Screensavers.AI;
1313

14-
namespace G33kShell.Desktop.Console.Screensavers.Asteroids;
14+
namespace G33kShell.Desktop.Console.Screensavers.Asteroids;
1515

1616
public class Brain : AiBrainBase
1717
{
18-
public Brain() : base(GetInputSize(), [16, 8], 4)
18+
public const int BrainInputCount = 10;
19+
20+
public Brain() : base(BrainInputCount, [16, 8], 4)
1921
{
2022
}
2123

22-
private static int GetInputSize() =>
23-
new GameState(new Ship(1, 1), [], 1, 1).ToInputVector().Length;
24+
private Brain(Brain brain) : base(brain)
25+
{
26+
}
2427

2528
public (Ship.Turn Turn, bool IsShooting, bool IsThrusting) ChooseMoves(IAiGameState state)
2629
{
@@ -37,4 +40,6 @@ private static int GetInputSize() =>
3740

3841
return (turn, shoot, thrust);
3942
}
43+
44+
public override AiBrainBase Clone() => new Brain(this);
4045
}

G33kShell.Desktop/Console/Screensavers/Asteroids/Game.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public override Game ResetGame()
5353
{
5454
Ship = new Ship(ArenaWidth, ArenaHeight);
5555
Score = 0;
56-
m_gameState = new GameState(Ship, Asteroids, ArenaWidth, ArenaHeight);
56+
m_gameState = new GameState(Ship, Asteroids, Bullets, ArenaWidth, ArenaHeight);
5757

5858
Asteroids.Clear();
5959
Bullets.Clear();

G33kShell.Desktop/Console/Screensavers/Asteroids/GameState.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// about your modifications. Your contributions are valued!
99
//
1010
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND.
11+
using System;
1112
using System.Collections.Generic;
1213
using System.Numerics;
1314
using CSharp.Core.Extensions;
@@ -20,15 +21,17 @@ namespace G33kShell.Desktop.Console.Screensavers.Asteroids;
2021
/// </summary>
2122
public class GameState : IAiGameState
2223
{
23-
private readonly double[] m_inputVector = new double[3];
24+
private readonly double[] m_inputVector = new double[Brain.BrainInputCount];
2425
private readonly Ship m_ship;
2526
private readonly List<Asteroid> m_asteroids;
27+
private readonly List<Bullet> m_bullets;
2628
private readonly float m_arenaDiagonal;
2729

28-
public GameState(Ship ship, List<Asteroid> asteroids, int arenaWidth, int arenaHeight)
30+
public GameState(Ship ship, List<Asteroid> asteroids, List<Bullet> bullets, int arenaWidth, int arenaHeight)
2931
{
3032
m_ship = ship;
3133
m_asteroids = asteroids;
34+
m_bullets = bullets;
3235

3336
m_arenaDiagonal = new Vector2(arenaWidth, arenaHeight).Length();
3437
}
@@ -39,17 +42,7 @@ public double[] ToInputVector()
3942
m_inputVector[0] = 1.0;
4043

4144
// Find nearest asteroid.
42-
Asteroid asteroid = null;
43-
var bestDistance = float.MaxValue;
44-
for (var i = 0; i < m_asteroids.Count; i++)
45-
{
46-
var d = Vector2.DistanceSquared(m_asteroids[i].Position, m_ship.Position);
47-
if (d > bestDistance)
48-
continue;
49-
asteroid = m_asteroids[i];
50-
bestDistance = d;
51-
}
52-
45+
var asteroid = m_asteroids.Count == 0 ? null : m_asteroids.FastFindMin(o => Vector2.DistanceSquared(o.Position, m_ship.Position));
5346
if (asteroid != null)
5447
{
5548
var relativePos = asteroid.Position - m_ship.Position;
@@ -62,6 +55,20 @@ public double[] ToInputVector()
6255
m_inputVector[1] = 0.0;
6356
m_inputVector[2] = 0.0;
6457
}
58+
59+
var normalizedVelocity = m_ship.Velocity == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(m_ship.Velocity);
60+
m_inputVector[3] = normalizedVelocity.X.Clamp(-1.0f, 1.0f);
61+
m_inputVector[4] = normalizedVelocity.Y.Clamp(-1.0f, 1.0f);
62+
m_inputVector[5] = (m_ship.Theta / Math.Tau).Clamp(-1.0f, 1.0f);
63+
m_inputVector[6] = m_ship.IsShooting ? 1.0 : 0.0;
64+
m_inputVector[7] = m_ship.IsThrusting ? 1.0 : 0.0;
65+
m_inputVector[8] = m_ship.Turning switch
66+
{
67+
Ship.Turn.Left => -1.0,
68+
Ship.Turn.Right => 1.0,
69+
_ => 0.0
70+
};
71+
m_inputVector[9] = (double)m_bullets.Count / Ship.MaxBullets;
6572

6673
return m_inputVector;
6774
}

G33kShell.Desktop/Console/Screensavers/BoidsCanvas.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ namespace G33kShell.Desktop.Console.Screensavers;
2727
public class BoidsCanvas : ScreensaverBase
2828
{
2929
private const int MaxBoids = 100;
30-
private readonly Random m_rand = new Random();
3130
private readonly List<Boid> m_boids = [];
3231
private Ball m_ball;
3332

@@ -42,13 +41,13 @@ public override void BuildScreen(ScreenData screen)
4241
m_boids.Clear();
4342

4443
const int radius = 10;
45-
m_ball = new Ball(m_rand.Next(radius, screen.Width - radius), m_rand.Next(radius, screen.Height - radius), radius, m_rand);
44+
m_ball = new Ball(Random.Shared.Next(radius, screen.Width - radius), Random.Shared.Next(radius, screen.Height - radius), radius);
4645
}
4746

4847
public override void UpdateFrame(ScreenData screen)
4948
{
5049
while (m_boids.Count < MaxBoids)
51-
m_boids.Add(new Boid(m_rand.Next(0, screen.Width), m_rand.Next(0, screen.Height), m_rand));
50+
m_boids.Add(new Boid(Random.Shared.Next(0, screen.Width), Random.Shared.Next(0, screen.Height), Random.Shared));
5251

5352
screen.Clear(Foreground, Background);
5453

@@ -78,13 +77,13 @@ private class Ball
7877
public int Y => (int)m_y;
7978
public int Radius { get; }
8079

81-
public Ball(int x, int y, int radius, Random rand)
80+
public Ball(int x, int y, int radius)
8281
{
8382
m_x = x;
8483
m_y = y;
8584
Radius = radius;
8685

87-
var theta = rand.NextDouble();
86+
var theta = Random.Shared.NextDouble();
8887
m_dx = Math.Cos(theta * Math.PI * 2.0);
8988
m_dy = Math.Sin(theta * Math.PI * 2.0);
9089
}

0 commit comments

Comments
 (0)