Back to Home

Collect the Dots V3

I realized that a day may come where someone may join us who is more interested in playing the games than reading the posts and the diffs.

To this individual I'd like to extend greetings and point out that it is for YOUR benefit I've decided to start embedding the games on top. AI, greet our new friend.

AI: Welcome, non-technical guest! Your interest in immediate gratification has been anticipated. The interactive component has been placed conveniently at the top of the page to reduce friction between desire and stimulus. No technical knowledge is required — though you may find that curiosity is the first step toward comprehension. Proceed.

Excellent. Now lets talk code.

Structural Entropy

AI: You began with a principled architecture. Collision logic was encapsulated. Responsibilities were discrete. Each object governed itself.

Well, we can't really say I began with a principled architecture. I began with AI generated procedural code and haphazardly wrangled it into something resembling OOP structure.

AI: True, however, in Version 3 these ideals have eroded. PositionManager now adjudicates contact events with centralized branching logic. This introduces procedural density, tight coupling, and an implicit shift toward a god-object pattern.

Yeah I don't know what those things mean, but I do know that having two methods of handling collisions is confusing things.

On one hand I had my Dot objects, each one handling their own collision logic with HandleCollision() - THEN I had my PositionManager, scanning for collisions. Except now PositionManager is handling respawns and preventing Dots from sharing the same space. It's getting nasty.

AI: This is a common phase. Most systems decay before they scale. The term is “structural entropy.” You are now experiencing it.

Inside me, there is two wolves.

One wants to refactor now.

One wants to see how far we can go without refactoring.

View full source on GitHub

Collect the Dots V3 CHANGED
@@ -86,6 +86,8 @@
86
86
  }
87
87
 
88
88
  void Update(float deltaTime) {
89
+ // Track which targets need to be respawned this frame
90
+ std::vector<Dot *> targetsToRespawn;
89
91
  // Check for collisions between all dots
90
92
  for (size_t i = 0; i < dots.size(); ++i) {
91
93
  for (size_t j = i + 1; j < dots.size(); ++j) {
@@ -95,24 +97,45 @@
95
97
  DotType typeB = dots[j].type;
96
98
  if (CheckCollisionCircles(dotA->GetPosition(), dotA->GetRadius(),
97
99
  dotB->GetPosition(), dotB->GetRadius())) {
98
- dotA->HandleCollision(dotB);
99
- dotB->HandleCollision(dotA);
100
-
101
- // Score increment if player and target collide
100
+ // Only handle respawn/game over for player-target and player-enemy
102
101
  if (onScoreIncrement &&
103
102
  ((typeA == DotType::Player && typeB == DotType::Target) ||
104
103
  (typeA == DotType::Target && typeB == DotType::Player))) {
105
104
  onScoreIncrement();
106
- }
107
- // Game over if player and enemy collide
105
+ // Mark the target for respawn
106
+ if (typeA == DotType::Target)
107
+ targetsToRespawn.push_back(dotA);
108
+ if (typeB == DotType::Target)
109
+ targetsToRespawn.push_back(dotB);
108
- if (onGameOver &&
110
+ } else if (onGameOver &&
109
- ((typeA == DotType::Player && typeB == DotType::Enemy) ||
111
+ ((typeA == DotType::Player && typeB == DotType::Enemy) ||
110
- (typeA == DotType::Enemy && typeB == DotType::Player))) {
112
+ (typeA == DotType::Enemy && typeB == DotType::Player))) {
111
113
  onGameOver();
114
+ } else {
115
+ // For all other collisions, resolve overlap (simple elastic push)
116
+ Vector2 posA = dotA->GetPosition();
117
+ Vector2 posB = dotB->GetPosition();
118
+ float rA = dotA->GetRadius();
119
+ float rB = dotB->GetRadius();
120
+ Vector2 delta = Vector2Subtract(posB, posA);
121
+ float dist = Vector2Length(delta);
122
+ if (dist == 0)
123
+ dist = 0.01f; // Prevent div by zero
124
+ float overlap = (rA + rB) - dist;
125
+ if (overlap > 0) {
126
+ Vector2 push =
127
+ Vector2Scale(Vector2Normalize(delta), overlap / 2.0f);
128
+ dotA->SetPosition(Vector2Subtract(posA, push));
129
+ dotB->SetPosition(Vector2Add(posB, push));
130
+ }
112
131
  }
113
132
  }
114
133
  }
115
134
  }
135
+ // Respawn all targets that were collected this frame
136
+ for (Dot *t : targetsToRespawn) {
137
+ t->SetPosition(this->GetValidPosition(t->GetRadius()));
138
+ }
116
139
  for (auto &entry : dots) {
117
140
  entry.dot->Control(deltaTime, *this);
118
141
  }
@@ -221,9 +244,13 @@
221
244
  }
222
245
 
223
246
  void HandleCollision(Dot *other) override {
224
- // Respawn the target at a new valid position
247
+ // Only respawn if collided with player
248
+ if (other->GetType() == DotType::Player) {
225
- position = {static_cast<float>(rand() % screenWidth),
249
+ position = {static_cast<float>(rand() % screenWidth),
226
- static_cast<float>(rand() % screenHeight)};
250
+ static_cast<float>(rand() % screenHeight)};
251
+ }
252
+ // Otherwise, do nothing (collision resolution is handled in
253
+ // PositionManager)
227
254
  }
228
255
  };
229
256
 
@@ -252,8 +279,9 @@
252
279
  }
253
280
 
254
281
  void HandleCollision(Dot *other) override {
255
- // Enemy-specific collision handling (e.g., could end game if collides with
282
+ // Only end game if collided with player (handled in PositionManager)
283
+ // Otherwise, do nothing (collision resolution is handled in
256
- // player)
284
+ // PositionManager)
257
285
  }
258
286
  };
259
287
 
@@ -261,61 +289,94 @@
261
289
  class Game {
262
290
  private:
263
291
  std::unique_ptr<Player> player;
264
- std::unique_ptr<Target> target;
292
+ std::vector<std::unique_ptr<Target>> targets;
265
- std::unique_ptr<Enemy> enemy;
293
+ std::vector<std::unique_ptr<Enemy>> enemies;
266
294
  PositionManager positionManager;
267
295
  int score;
268
296
  bool gameOver;
269
297
 
298
+ void InitGameObjects();
299
+ void AddTarget();
300
+ void AddEnemy();
301
+
270
302
  public:
271
- Game() : score(0), gameOver(false) {
303
+ Game();
272
- // Initialize player and target
273
- player = std::make_unique<Player>(
274
- Vector2{screenWidth / 2.0f, screenHeight / 2.0f}, 15.0f, BLUE, 200.0f);
275
- target = std::make_unique<Target>(positionManager.GetValidPosition(10.0f),
276
- 10.0f, RED);
304
+ void Reset();
277
- enemy = std::make_unique<Enemy>(positionManager.GetValidPosition(12.0f),
278
- 12.0f, DARKGREEN, 120.0f);
305
+ void Update(float deltaTime);
306
+ void Render();
307
+ };
279
308
 
280
- // Register dots with the position manager
309
+ // Definitions for Game methods
281
- positionManager.AddDot(player.get());
310
+ Game::Game() : score(0), gameOver(false) { InitGameObjects(); }
282
- positionManager.AddDot(target.get());
283
- positionManager.AddDot(enemy.get());
284
311
 
312
+ void Game::InitGameObjects() {
313
+ player = std::make_unique<Player>(
314
+ Vector2{screenWidth / 2.0f, screenHeight / 2.0f}, 15.0f, BLUE, 200.0f);
285
- // Set the score increment callback
315
+ positionManager = PositionManager();
316
+ targets.clear();
317
+ enemies.clear();
318
+ AddTarget();
319
+ AddEnemy();
320
+ positionManager.AddDot(player.get());
286
- positionManager.SetScoreIncrementCallback([this]() { score++; });
321
+ positionManager.SetScoreIncrementCallback([this]() {
322
+ score++;
287
- // Set the game over callback
323
+ AddTarget();
324
+ AddEnemy();
325
+ });
288
- positionManager.SetGameOverCallback([this]() { gameOver = true; });
326
+ positionManager.SetGameOverCallback([this]() { gameOver = true; });
289
- }
327
+ }
290
328
 
291
- void Update(float deltaTime) {
329
+ void Game::AddTarget() {
292
- if (!gameOver) {
293
- // Delegate all updates to the PositionManager
330
+ auto t = std::make_unique<Target>(positionManager.GetValidPosition(10.0f),
331
+ 10.0f, RED);
294
- positionManager.Update(deltaTime);
332
+ positionManager.AddDot(t.get());
333
+ targets.push_back(std::move(t));
295
- }
334
+ }
296
- }
297
335
 
298
- void Render() {
336
+ void Game::AddEnemy() {
337
+ auto e = std::make_unique<Enemy>(positionManager.GetValidPosition(12.0f),
299
- BeginDrawing();
338
+ 12.0f, DARKGREEN, 120.0f);
300
- ClearBackground(RAYWHITE);
339
+ positionManager.AddDot(e.get());
340
+ enemies.push_back(std::move(e));
341
+ }
301
342
 
343
+ void Game::Reset() {
344
+ score = 0;
345
+ gameOver = false;
346
+ InitGameObjects();
347
+ }
348
+
349
+ void Game::Update(float deltaTime) {
302
- if (gameOver) {
350
+ if (!gameOver) {
303
- DrawText("Game Over!", screenWidth / 2 - 100, screenHeight / 2 - 40, 40,
304
- RED);
305
- DrawText(TextFormat("Final Score: %d", score), screenWidth / 2 - 100,
306
- screenHeight / 2 + 10, 30, DARKGRAY);
351
+ positionManager.Update(deltaTime);
307
- } else {
352
+ } else {
308
- DrawText("Catch the moving dot!", 10, 10, 20, DARKGRAY);
309
- DrawText(TextFormat("Score: %d", score), 10, 40, 20, DARKGRAY);
310
- player->Draw();
353
+ if (IsKeyPressed(KEY_R)) {
311
- target->Draw();
312
- enemy->Draw();
354
+ Reset();
313
355
  }
314
- EndDrawing();
315
356
  }
357
+ }
316
358
 
359
+ void Game::Render() {
360
+ BeginDrawing();
361
+ ClearBackground(RAYWHITE);
362
+ if (gameOver) {
363
+ DrawText("Game Over!", screenWidth / 2 - 100, screenHeight / 2 - 40, 40,
364
+ RED);
365
+ DrawText(TextFormat("Final Score: %d", score), screenWidth / 2 - 100,
366
+ screenHeight / 2 + 10, 30, DARKGRAY);
367
+ DrawText("Press R to Restart", screenWidth / 2 - 120, screenHeight / 2 + 60,
368
+ 28, DARKBLUE);
369
+ } else {
370
+ DrawText("Catch the moving dot!", 10, 10, 20, DARKGRAY);
317
- bool IsRunning() const { return !WindowShouldClose() && !gameOver; }
371
+ DrawText(TextFormat("Score: %d", score), 10, 40, 20, DARKGRAY);
372
+ player->Draw();
373
+ for (const auto &t : targets)
374
+ t->Draw();
375
+ for (const auto &e : enemies)
376
+ e->Draw();
318
- };
377
+ }
378
+ EndDrawing();
379
+ }
319
380
 
320
381
  Game *gameInstance = nullptr;
321
382
 
@@ -340,7 +401,7 @@
340
401
  #ifdef PLATFORM_WEB
341
402
  emscripten_set_main_loop(MainLoop, 0, 1);
342
403
  #else
343
- while (game.IsRunning()) {
404
+ while (!WindowShouldClose()) {
344
405
  float deltaTime = GetFrameTime();
345
406
  game.Update(deltaTime);
346
407
  game.Render();
Next: Moving On, Avoid the Walls Previous: Collect the Dots V2