FluxSand 1.0
FluxSand - Interactive Digital Hourglass
Loading...
Searching...
No Matches
comp_sand.hpp
1#pragma once
2
3#include <array>
4#include <cmath>
5#include <random>
6#include <utility>
7#include <vector>
8
9class SandGrid {
10 public:
11 static constexpr int SIZE = 16;
12 static constexpr float PI = 3.14159265f;
13
14 const std::array<std::array<bool, SIZE>, SIZE>& GetGrid() const {
15 return grid_;
16 }
17
18 bool GetCell(int r, int c) const {
19 return InBounds(r, c) ? grid_[r][c] : false;
20 }
21
22 void SetCell(int r, int c, bool val) {
23 if (InBounds(r, c)) grid_[r][c] = val;
24 }
25
26 bool AddNewSand() {
27 if (grid_[15][15] == false) {
28 grid_[15][15] = true;
29 return true;
30 } else {
31 return false;
32 }
33 }
34
35 bool AddGrainNearExisting() {
36 std::vector<std::pair<int, int>> candidates;
37 for (int row = 0; row < SIZE; ++row) {
38 for (int col = 0; col < SIZE; ++col) {
39 if (grid_[row][col]) {
40 for (int dr = -1; dr <= 1; ++dr) {
41 for (int dc = -1; dc <= 1; ++dc) {
42 if (dr == 0 && dc == 0) continue;
43 int nr = row + dr;
44 int nc = col + dc;
45 if (InBounds(nr, nc) && !grid_[nr][nc]) {
46 candidates.emplace_back(nr, nc);
47 }
48 }
49 }
50 }
51 }
52 }
53
54 if (candidates.empty()) return false;
55 std::uniform_int_distribution<int> dist(0, candidates.size() - 1);
56 auto [r, c] = candidates[dist(rng_)];
57 grid_[r][c] = true;
58 return true;
59 }
60
61 void StepOnce(float gravity_deg) {
62 // Rotate gravity by 225 degrees to align with visual direction
63 gravity_deg += 225.0f;
64 if (gravity_deg >= 360.0f) gravity_deg -= 360.0f;
65
66 // Convert gravity direction to unit vector (gx, gy)
67 float angle_rad = gravity_deg * PI / 180.0f;
68 float gx = std::cos(angle_rad);
69 float gy = std::sin(angle_rad);
70
71 // Define all 8 possible neighboring directions
72 static const std::vector<std::pair<int, int>> directions = {
73 {-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1},
74 };
75
76 // Random angle noise for more natural flow
77 std::uniform_real_distribution<float> noise_dist(-30.0f, 30.0f);
78
79 // List of sand moves to perform this frame: (from_row, from_col, to_row,
80 // to_col)
81 std::vector<std::tuple<int, int, int, int>> moves;
82
83 // Tracks which cells are already targeted to avoid collision
84 std::array<std::array<bool, SIZE>, SIZE> occupied_next = {};
85
86 // ==== Custom row-wise center-outward traversal (bottom-up) ====
87 std::vector<std::pair<int, int>> traversal_order;
88 for (int r = SIZE - 1; r >= 0; --r) {
89 int center = SIZE / 2;
90 traversal_order.emplace_back(r, center);
91 for (int offset = 1; offset < SIZE; ++offset) {
92 int left = center - offset;
93 int right = center + offset;
94 if (left >= 0) traversal_order.emplace_back(r, left);
95 if (right < SIZE) traversal_order.emplace_back(r, right);
96 }
97 }
98
99 // ==== Traverse and decide moves ====
100 for (auto [r, c] : traversal_order) {
101 if (!grid_[r][c]) continue; // Skip if no sand
102
103 float noise_deg = noise_dist(rng_);
104 float cos_threshold = std::cos((55.0f + noise_deg) * PI / 180.0f);
105
106 std::pair<int, int> best_move = {0, 0};
107 float best_dot = -2.0f;
108
109 // Try all directions to find the best move
110 for (auto [dr, dc] : directions) {
111 int nr = r + dr;
112 int nc = c + dc;
113 if (!InBounds(nr, nc) || grid_[nr][nc])
114 continue; // Skip if out of bounds or not empty
115
116 float vx = static_cast<float>(dc);
117 float vy = static_cast<float>(dr);
118 float len = std::sqrt(vx * vx + vy * vy);
119 if (len == 0.0f) continue;
120
121 vx /= len;
122 vy /= len;
123
124 // Dot product with gravity vector
125 float dot = vx * gx + vy * gy;
126
127 // Select move if direction is closer to gravity and exceeds threshold
128 if (dot > cos_threshold && dot > best_dot) {
129 best_dot = dot;
130 best_move = {dr, dc};
131 }
132 }
133
134 // Queue the move if a good direction was found and not already taken
135 if (best_dot > -1.0f) {
136 int nr = r + best_move.first;
137 int nc = c + best_move.second;
138
139 if (!occupied_next[nr][nc]) {
140 occupied_next[nr][nc] = true;
141 moves.emplace_back(r, c, nr, nc);
142 }
143 }
144 }
145
146 // ==== Perform all queued moves ====
147 for (auto [r, c, nr, nc] : moves) {
148 grid_[r][c] = false;
149 grid_[nr][nc] = true;
150 }
151
152 // Optional: Debug output
153 // std::cout << "[StepOnce] Moved " << moves.size() << " sand particles\n";
154 }
155
156 void Clear() {
157 for (auto& row : grid_) {
158 row.fill(false);
159 }
160 }
161
162 int Count() const {
163 int total = 0;
164 for (const auto& row : grid_) {
165 for (bool v : row) {
166 total += v;
167 }
168 }
169 return total;
170 }
171
172 static bool MoveSand(SandGrid* up, SandGrid* down, float angle) {
173 if (angle < 90 || angle > 270) {
174 if (up->grid_[0][0] && !down->grid_[15][15]) {
175 down->grid_[15][15] = true;
176 up->grid_[0][0] = false;
177 return true;
178 }
179 } else {
180 if (!up->grid_[0][0] && down->grid_[15][15]) {
181 up->grid_[0][0] = true;
182 down->grid_[15][15] = false;
183 return true;
184 }
185 }
186 return false;
187 }
188
189 void RunUnitTest() {
190 std::cout << "[SandGrid::UnitTest] Starting sand grid test...\n";
191
192 SandGrid test;
193 test.Clear();
194
195 // Test AddNewSand
196 bool added = test.AddNewSand();
197 std::cout << std::format("[Test] AddNewSand → {}\n",
198 added ? "✅ Success" : "❌ Failed");
199
200 int before = test.Count();
201 test.StepOnce(0.0f);
202 int after = test.Count();
203 std::cout << std::format("[Test] StepOnce → Particle count: {} → {}\n",
204 before, after);
205
206 // Test MoveSand (grid transfer)
207 SandGrid up, down;
208 up.Clear();
209 down.Clear();
210 up.SetCell(0, 0, true);
211 bool moved = SandGrid::MoveSand(&up, &down, 45.0f);
212 std::cout << std::format("[Test] MoveSand(→down) → {}\n",
213 moved ? "✅ Success" : "❌ Failed");
214
215 // Test Clear
216 test.Clear();
217 std::cout << std::format("[Test] Clear → Count after clear: {}\n",
218 test.Count());
219
220 // StepOnce performance test
221 std::cout << "[Perf] Running StepOnce 100 times...\n";
222 for (int i = 0; i < 50; ++i) {
223 test.AddGrainNearExisting(); // Fill a bit
224 }
225
226 std::vector<float> times;
227 times.reserve(100);
228 for (int i = 0; i < 100; ++i) {
229 auto start = std::chrono::high_resolution_clock::now();
230 test.StepOnce(0.0f);
231 auto end = std::chrono::high_resolution_clock::now();
232 float ms = std::chrono::duration<float, std::micro>(end - start).count();
233 times.push_back(ms);
234 }
235
236 auto [min_it, max_it] = std::minmax_element(times.begin(), times.end());
237 float avg =
238 std::accumulate(times.begin(), times.end(), 0.0f) / times.size();
239
240 std::cout << std::format(
241 "[Perf] StepOnce timing (µs): min = {:>6.2f}, max = {:>6.2f}, avg = "
242 "{:>6.2f}\n",
243 *min_it, *max_it, avg);
244
245 std::cout << "[SandGrid::UnitTest] ✅ Test complete.\n";
246 }
247
248 private:
249 bool InBounds(int r, int c) const {
250 return r >= 0 && r < SIZE && c >= 0 && c < SIZE;
251 }
252
253 std::array<std::array<bool, SIZE>, SIZE> grid_ = {};
254
255 std::mt19937 rng_{std::random_device{}()};
256};