@@ -23,16 +23,18 @@ namespace G33kShell.Desktop.Console.Screensavers.AI;
2323/// </summary>
2424public abstract class AiGameCanvasBase : ScreensaverBase
2525{
26- private int m_generation ;
27- private double m_savedRating ;
2826 private const int InitialPopSize = 300 ;
2927 private const int MinPopSize = 150 ;
28+ private const int MaxGoatBrains = 5 ;
29+
30+ private readonly List < ( double Rating , AiBrainBase Brain ) > m_goatBrains = new List < ( double Rating , AiBrainBase Brain ) > ( MaxGoatBrains ) ;
31+ private int m_generation ;
32+ private double m_savedRating ;
3033 private int m_generationsSinceImprovement ;
3134 private int m_currentPopSize = InitialPopSize ;
3235 private Task m_trainingTask ;
3336 private bool m_stopTraining ;
34-
35- protected AiGameBase [ ] m_games ;
37+ private List < AiBrainBase > m_nextGenBrains ;
3638
3739 protected int ArenaWidth { get ; }
3840 protected int ArenaHeight { get ; }
@@ -83,35 +85,70 @@ public override void StopScreensaver()
8385
8486 private void TrainAiImpl ( Action < byte [ ] > saveBrainBytes )
8587 {
86- m_games ??= Enumerable . Range ( 0 , m_currentPopSize ) . Select ( _ => CreateGameWithSeed ( m_generation ) ) . ToArray ( ) ;
88+ m_nextGenBrains ??= Enumerable . Range ( 0 , InitialPopSize ) . Select ( _ => CreateGame ( ) . Brain ) . ToList ( ) ;
8789
88- Parallel . For ( 0 , m_games . Length , i =>
90+ var games = new ( double AverageRating , AiGameBase Game , AiBrainBase Brain ) [ m_nextGenBrains . Count ] ;
91+ Parallel . For ( 0 , games . Length , i =>
8992 {
90- var game = m_games [ i ] ;
91- while ( ! game . IsGameOver )
92- game . Tick ( ) ;
93+ // Play the base game.
94+ var baseGame = CreateGameWithSeed ( m_generation ) ;
95+ while ( ! baseGame . IsGameOver && ! m_stopTraining )
96+ baseGame . Tick ( ) ;
97+
98+ var totalRating = baseGame . Rating ;
99+ if ( baseGame . Rating > 0.0 )
100+ {
101+ // Play several more games.
102+ var gameCount = 1 ;
103+ for ( var trial = 0 ; trial < 4 && ! m_stopTraining ; trial ++ , gameCount ++ )
104+ {
105+ var game = CreateGameWithSeed ( Random . Shared . Next ( ) ) ;
106+ game . Brain = baseGame . Brain ;
107+ while ( ! game . IsGameOver )
108+ game . Tick ( ) ;
109+ totalRating += game . Rating ;
110+ if ( game . Rating <= 0.0001 )
111+ break ; // No score, no point in continuing.
112+ }
113+
114+ totalRating /= gameCount ;
115+ }
116+
117+ games [ i ] = ( totalRating , baseGame , baseGame . Brain ) ;
93118 } ) ;
94119
95120 // Select the breeders.
96- var orderedGames = m_games . OrderByDescending ( o => o . Rating ) . ToArray ( ) ;
97-
121+ var orderedGames = games . OrderByDescending ( o => o . AverageRating ) . ToArray ( ) ;
122+ var theBest = orderedGames [ 0 ] ;
123+
98124 // Report summary of results.
99125 m_generation ++ ;
100- var veryBest = orderedGames [ 0 ] ;
101- var stats = $ "Gen { m_generation } |Pop { m_currentPopSize } |Rating { veryBest . Rating : F1} |GOAT { m_savedRating : F1} ";
102- var extraStats = veryBest . ExtraGameStats ( ) . Select ( o => $ " { o . Name } : { o . Value } ") . ToArray ( ) . ToCsv ( ) . Trim ( ) ;
126+ var stats = $ "Gen { m_generation } |Pop { m_currentPopSize } |Rating { theBest . AverageRating : F1} |GOAT { m_savedRating : F1} ";
127+ var extraStats = theBest . Game . ExtraGameStats ( ) . Select ( o => $ " { o . Name } : { o . Value } ") . ToArray ( ) . ToCsv ( ) . Trim ( ) ;
103128 if ( ! string . IsNullOrEmpty ( extraStats ) )
104129 stats += $ "|{ extraStats } ";
105130 System . Console . WriteLine ( stats ) ;
131+
132+ // Remember the GOAT brains.
133+ var worstGoatRating = m_goatBrains . Count > 0 ? m_goatBrains . FastFindMin ( o => o . Rating ) . Rating : 0.0 ;
134+ for ( var i = 0 ; i < orderedGames . Length ; i ++ )
135+ {
136+ if ( orderedGames [ i ] . AverageRating > worstGoatRating )
137+ m_goatBrains . Add ( ( orderedGames [ i ] . AverageRating , orderedGames [ i ] . Brain ) ) ;
138+ }
139+
140+ while ( m_goatBrains . Count > MaxGoatBrains )
141+ {
142+ var toRemove = m_goatBrains . FastFindMin ( o => o . Rating ) ;
143+ m_goatBrains . Remove ( toRemove ) ;
144+ }
106145
107146 // Persist brain improvements.
108- AiBrainBase goatBrain = null ;
109- if ( veryBest . Rating > m_savedRating )
147+ if ( theBest . AverageRating > m_savedRating )
110148 {
111- m_savedRating = veryBest . Rating ;
149+ m_savedRating = theBest . AverageRating ;
112150 System . Console . WriteLine ( "Saved." ) ;
113- saveBrainBytes ( veryBest . Brain . Save ( ) ) ;
114- goatBrain = veryBest . Brain . Clone ( ) ;
151+ saveBrainBytes ( theBest . Brain . Save ( ) ) ;
115152
116153 m_generationsSinceImprovement = 0 ;
117154 }
@@ -124,39 +161,30 @@ private void TrainAiImpl(Action<byte[]> saveBrainBytes)
124161 {
125162 m_generationsSinceImprovement = 0 ;
126163 m_currentPopSize = InitialPopSize ;
127- System . Console . WriteLine ( "Stagnation detected — Increasing population size." ) ;
164+ System . Console . WriteLine ( "Stagnation detected - Increasing population size." ) ;
128165 }
129166 }
130167
131168 // Build the brains for the next generation.
132- var nextBrains = new List < AiBrainBase > ( m_games . Length ) ;
133-
134- // The GOAT lives on.
135- if ( goatBrain != null )
136- {
137- nextBrains . Add ( goatBrain . Clone ( ) ) ;
138- nextBrains . AddRange ( Enumerable . Range ( 0 , ( int ) ( m_currentPopSize * 0.05 ) ) . Select ( _ => goatBrain . Clone ( ) . Mutate ( 0.03 ) ) ) ;
139- }
169+ var nextBrains = new List < AiBrainBase > ( games . Length ) ;
170+ nextBrains . AddRange ( m_goatBrains . Select ( o => o . Brain . Clone ( ) ) ) ;
140171
141172 // Spawn 5% pure randoms.
142- nextBrains . AddRange ( Enumerable . Range ( 0 , ( int ) ( m_currentPopSize * 0.05 ) ) . Select ( _ => veryBest . Brain . Clone ( ) . Randomize ( ) ) ) ;
173+ nextBrains . AddRange ( Enumerable . Range ( 0 , ( int ) ( m_currentPopSize * 0.05 ) ) . Select ( _ => CreateGameWithSeed ( 0 ) . Brain . Clone ( ) . Randomize ( ) ) ) ;
143174
144175 // Elite get to be parents.
176+ var breeders = orderedGames . Select ( o => ( o . AverageRating , o . Brain ) ) . ToList ( ) ;
177+ breeders . AddRange ( m_goatBrains ) ;
145178 while ( nextBrains . Count < m_currentPopSize )
146179 {
147- var mumBrain = Random . Shared . RouletteSelection ( m_games , o => o . Rating ) . Brain ;
148- var dadBrain = Random . Shared . RouletteSelection ( m_games , o => o . Rating ) . Brain ;
180+ var mumBrain = Random . Shared . RouletteSelection ( breeders , o => o . AverageRating ) . Brain ;
181+ var dadBrain = Random . Shared . RouletteSelection ( breeders , o => o . AverageRating ) . Brain ;
149182 var childBrain = mumBrain . Clone ( ) . CrossWith ( dadBrain , 0.5 ) . Mutate ( 0.05 ) ;
150183 nextBrains . Add ( childBrain ) ;
151184 }
152185
153186 // Make the next generation of games.
154- m_games = nextBrains . Select ( o =>
155- {
156- var newGame = CreateGameWithSeed ( m_generation ) ;
157- newGame . Brain = o ;
158- return newGame ;
159- } ) . ToArray ( ) ;
187+ m_nextGenBrains = nextBrains ;
160188 }
161189
162190 private AiGameBase CreateGameWithSeed ( int seed )
0 commit comments