trianglengin 2.0.1__cp310-cp310-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. trianglengin/__init__.py +35 -0
  2. trianglengin/config/README.md +38 -0
  3. trianglengin/config/__init__.py +8 -0
  4. trianglengin/config/display_config.py +47 -0
  5. trianglengin/config/env_config.py +62 -0
  6. trianglengin/core/__init__.py +10 -0
  7. trianglengin/cpp/CMakeLists.txt +42 -0
  8. trianglengin/cpp/bindings.cpp +211 -0
  9. trianglengin/cpp/config.h +28 -0
  10. trianglengin/cpp/game_state.cpp +327 -0
  11. trianglengin/cpp/game_state.h +73 -0
  12. trianglengin/cpp/grid_data.cpp +239 -0
  13. trianglengin/cpp/grid_data.h +78 -0
  14. trianglengin/cpp/grid_logic.cpp +125 -0
  15. trianglengin/cpp/grid_logic.h +30 -0
  16. trianglengin/cpp/shape_logic.cpp +100 -0
  17. trianglengin/cpp/shape_logic.h +28 -0
  18. trianglengin/cpp/structs.h +40 -0
  19. trianglengin/game_interface.py +222 -0
  20. trianglengin/py.typed +0 -0
  21. trianglengin/trianglengin_cpp.cpython-310-darwin.so +0 -0
  22. trianglengin/ui/README.md +35 -0
  23. trianglengin/ui/__init__.py +21 -0
  24. trianglengin/ui/app.py +107 -0
  25. trianglengin/ui/cli.py +123 -0
  26. trianglengin/ui/config.py +44 -0
  27. trianglengin/ui/interaction/README.md +44 -0
  28. trianglengin/ui/interaction/__init__.py +19 -0
  29. trianglengin/ui/interaction/debug_mode_handler.py +72 -0
  30. trianglengin/ui/interaction/event_processor.py +49 -0
  31. trianglengin/ui/interaction/input_handler.py +89 -0
  32. trianglengin/ui/interaction/play_mode_handler.py +156 -0
  33. trianglengin/ui/visualization/README.md +42 -0
  34. trianglengin/ui/visualization/__init__.py +58 -0
  35. trianglengin/ui/visualization/core/README.md +51 -0
  36. trianglengin/ui/visualization/core/__init__.py +16 -0
  37. trianglengin/ui/visualization/core/colors.py +115 -0
  38. trianglengin/ui/visualization/core/coord_mapper.py +85 -0
  39. trianglengin/ui/visualization/core/fonts.py +65 -0
  40. trianglengin/ui/visualization/core/layout.py +77 -0
  41. trianglengin/ui/visualization/core/visualizer.py +248 -0
  42. trianglengin/ui/visualization/drawing/README.md +49 -0
  43. trianglengin/ui/visualization/drawing/__init__.py +43 -0
  44. trianglengin/ui/visualization/drawing/grid.py +213 -0
  45. trianglengin/ui/visualization/drawing/highlight.py +31 -0
  46. trianglengin/ui/visualization/drawing/hud.py +43 -0
  47. trianglengin/ui/visualization/drawing/previews.py +181 -0
  48. trianglengin/ui/visualization/drawing/shapes.py +46 -0
  49. trianglengin/ui/visualization/drawing/utils.py +23 -0
  50. trianglengin/utils/__init__.py +9 -0
  51. trianglengin/utils/geometry.py +62 -0
  52. trianglengin/utils/types.py +10 -0
  53. trianglengin-2.0.1.dist-info/METADATA +250 -0
  54. trianglengin-2.0.1.dist-info/RECORD +58 -0
  55. trianglengin-2.0.1.dist-info/WHEEL +5 -0
  56. trianglengin-2.0.1.dist-info/entry_points.txt +2 -0
  57. trianglengin-2.0.1.dist-info/licenses/LICENSE +22 -0
  58. trianglengin-2.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,327 @@
1
+ // File: src/trianglengin/cpp/game_state.cpp
2
+ #include "game_state.h"
3
+ #include "grid_logic.h"
4
+ #include "shape_logic.h"
5
+ #include <stdexcept>
6
+ #include <numeric>
7
+ #include <iostream>
8
+ #include <algorithm> // For std::min
9
+
10
+ namespace trianglengin::cpp
11
+ {
12
+
13
+ GameStateCpp::GameStateCpp(const EnvConfigCpp &config, unsigned int initial_seed)
14
+ : config_(config),
15
+ grid_data_(config_),
16
+ shapes_(config_.num_shape_slots),
17
+ score_(0.0),
18
+ current_step_(0),
19
+ game_over_(false),
20
+ rng_(initial_seed)
21
+ {
22
+ reset();
23
+ }
24
+
25
+ void GameStateCpp::reset()
26
+ {
27
+ grid_data_.reset();
28
+ std::fill(shapes_.begin(), shapes_.end(), std::nullopt);
29
+ score_ = 0.0;
30
+ current_step_ = 0;
31
+ game_over_ = false;
32
+ game_over_reason_ = std::nullopt;
33
+ valid_actions_cache_ = std::nullopt;
34
+ shape_logic::refill_shape_slots(*this, rng_);
35
+ check_initial_state_game_over();
36
+ }
37
+
38
+ void GameStateCpp::check_initial_state_game_over()
39
+ {
40
+ get_valid_actions(true);
41
+ if (!game_over_ && valid_actions_cache_ && valid_actions_cache_->empty())
42
+ {
43
+ force_game_over("No valid actions available at start.");
44
+ }
45
+ }
46
+
47
+ std::tuple<double, bool> GameStateCpp::step(Action action)
48
+ {
49
+ if (game_over_)
50
+ {
51
+ return {0.0, true};
52
+ }
53
+
54
+ const auto &valid_actions = get_valid_actions();
55
+ if (valid_actions.find(action) == valid_actions.end())
56
+ {
57
+ // Invalid action detected
58
+ force_game_over("Invalid action provided: " + std::to_string(action));
59
+ // Apply penalty to score *before* returning
60
+ score_ += config_.penalty_game_over;
61
+ return {config_.penalty_game_over, true}; // Return penalty and done=true
62
+ }
63
+
64
+ int shape_idx, r, c;
65
+ try
66
+ {
67
+ std::tie(shape_idx, r, c) = decode_action(action);
68
+ }
69
+ catch (const std::out_of_range &e)
70
+ {
71
+ // Error during decoding (should be caught by valid_actions check, but defensive)
72
+ force_game_over("Failed to decode action: " + std::to_string(action));
73
+ score_ += config_.penalty_game_over; // Apply penalty
74
+ return {config_.penalty_game_over, true};
75
+ }
76
+
77
+ if (shape_idx < 0 || shape_idx >= static_cast<int>(shapes_.size()) || !shapes_[shape_idx].has_value())
78
+ {
79
+ // Action references invalid slot (should be caught by valid_actions check)
80
+ force_game_over("Action references invalid/empty shape slot: " + std::to_string(shape_idx));
81
+ score_ += config_.penalty_game_over; // Apply penalty
82
+ return {config_.penalty_game_over, true};
83
+ }
84
+
85
+ const ShapeCpp &shape_to_place = shapes_[shape_idx].value();
86
+
87
+ if (!grid_logic::can_place(grid_data_, shape_to_place, r, c))
88
+ {
89
+ // Should not happen if valid_actions is correct
90
+ force_game_over("Placement check failed for valid action (logic error?). Action: " + std::to_string(action));
91
+ score_ += config_.penalty_game_over; // Apply penalty
92
+ return {config_.penalty_game_over, true};
93
+ }
94
+
95
+ // --- Placement ---
96
+ std::set<Coord> newly_occupied_coords;
97
+ int placed_count = 0;
98
+ auto &occupied_grid = grid_data_.get_occupied_grid_mut();
99
+ auto &color_grid = grid_data_.get_color_id_grid_mut();
100
+ for (const auto &tri_data : shape_to_place.triangles)
101
+ {
102
+ int dr, dc;
103
+ bool is_up_ignored;
104
+ std::tie(dr, dc, is_up_ignored) = tri_data;
105
+ int target_r = r + dr;
106
+ int target_c = c + dc;
107
+ if (!grid_data_.is_valid(target_r, target_c) || grid_data_.is_death(target_r, target_c))
108
+ {
109
+ force_game_over("Attempted placement out of bounds/death zone during execution. Action: " + std::to_string(action));
110
+ score_ += config_.penalty_game_over; // Apply penalty
111
+ return {config_.penalty_game_over, true};
112
+ }
113
+ occupied_grid[target_r][target_c] = true;
114
+ color_grid[target_r][target_c] = static_cast<int8_t>(shape_to_place.color_id);
115
+ newly_occupied_coords.insert({target_r, target_c});
116
+ placed_count++;
117
+ }
118
+ shapes_[shape_idx] = std::nullopt;
119
+
120
+ // --- Line Clearing ---
121
+ int lines_cleared_count;
122
+ std::set<Coord> cleared_coords;
123
+ LineFsSet cleared_lines_fs;
124
+ std::tie(lines_cleared_count, cleared_coords, cleared_lines_fs) =
125
+ grid_logic::check_and_clear_lines(grid_data_, newly_occupied_coords);
126
+ int cleared_count = static_cast<int>(cleared_coords.size());
127
+
128
+ // --- Refill ---
129
+ bool all_slots_empty = true;
130
+ for (const auto &shape_opt : shapes_)
131
+ {
132
+ if (shape_opt.has_value())
133
+ {
134
+ all_slots_empty = false;
135
+ break;
136
+ }
137
+ }
138
+ if (all_slots_empty)
139
+ {
140
+ shape_logic::refill_shape_slots(*this, rng_);
141
+ }
142
+
143
+ // --- Update State & Check Game Over ---
144
+ current_step_++;
145
+ valid_actions_cache_ = std::nullopt;
146
+ get_valid_actions(true); // Force recalculation and update game_over_ if needed
147
+ // game_over_ flag is now definitive
148
+
149
+ // --- Calculate Reward & Update Score ---
150
+ double reward = 0.0;
151
+ reward += static_cast<double>(placed_count) * config_.reward_per_placed_triangle;
152
+ reward += static_cast<double>(cleared_count) * config_.reward_per_cleared_triangle;
153
+
154
+ if (game_over_)
155
+ {
156
+ reward += config_.penalty_game_over;
157
+ }
158
+ else
159
+ {
160
+ reward += config_.reward_per_step_alive;
161
+ }
162
+ score_ += reward; // Update score based on calculated reward
163
+
164
+ return {reward, game_over_};
165
+ }
166
+
167
+ bool GameStateCpp::is_over() const
168
+ {
169
+ if (game_over_)
170
+ return true;
171
+ if (valid_actions_cache_.has_value())
172
+ return valid_actions_cache_->empty();
173
+ calculate_valid_actions_internal();
174
+ return valid_actions_cache_->empty();
175
+ }
176
+
177
+ void GameStateCpp::force_game_over(const std::string &reason)
178
+ {
179
+ // This function only sets the flags and reason.
180
+ // Score update should happen in the context where the game over is triggered (e.g., step).
181
+ if (!game_over_)
182
+ {
183
+ game_over_ = true;
184
+ game_over_reason_ = reason;
185
+ valid_actions_cache_ = std::set<Action>();
186
+ }
187
+ }
188
+
189
+ double GameStateCpp::get_score() const
190
+ {
191
+ return score_;
192
+ }
193
+
194
+ const std::set<Action> &GameStateCpp::get_valid_actions(bool force_recalculate)
195
+ {
196
+ if (game_over_)
197
+ {
198
+ if (!valid_actions_cache_.has_value() || !valid_actions_cache_->empty())
199
+ {
200
+ valid_actions_cache_ = std::set<Action>();
201
+ }
202
+ return *valid_actions_cache_;
203
+ }
204
+ if (!force_recalculate && valid_actions_cache_.has_value())
205
+ {
206
+ return *valid_actions_cache_;
207
+ }
208
+ calculate_valid_actions_internal();
209
+ // Check if game should end *after* calculating actions
210
+ if (!game_over_ && valid_actions_cache_->empty())
211
+ {
212
+ force_game_over("No valid actions available.");
213
+ }
214
+ return *valid_actions_cache_;
215
+ }
216
+
217
+ void GameStateCpp::invalidate_action_cache()
218
+ {
219
+ valid_actions_cache_ = std::nullopt;
220
+ }
221
+
222
+ void GameStateCpp::calculate_valid_actions_internal() const
223
+ {
224
+ if (game_over_)
225
+ {
226
+ valid_actions_cache_ = std::set<Action>();
227
+ return;
228
+ }
229
+ std::set<Action> valid_actions;
230
+ for (int shape_idx = 0; shape_idx < static_cast<int>(shapes_.size()); ++shape_idx)
231
+ {
232
+ if (!shapes_[shape_idx].has_value())
233
+ continue;
234
+ const ShapeCpp &shape = shapes_[shape_idx].value();
235
+ for (int r = 0; r < config_.rows; ++r)
236
+ {
237
+ const auto &[start_c, end_c] = config_.playable_range_per_row[r];
238
+ for (int c = start_c; c < end_c; ++c)
239
+ {
240
+ if (grid_logic::can_place(grid_data_, shape, r, c))
241
+ {
242
+ valid_actions.insert(encode_action(shape_idx, r, c));
243
+ }
244
+ }
245
+ }
246
+ }
247
+ valid_actions_cache_ = std::move(valid_actions);
248
+ }
249
+
250
+ int GameStateCpp::get_current_step() const { return current_step_; }
251
+ std::optional<std::string> GameStateCpp::get_game_over_reason() const { return game_over_reason_; }
252
+
253
+ GameStateCpp GameStateCpp::copy() const
254
+ {
255
+ GameStateCpp newState = *this;
256
+ if (this->valid_actions_cache_.has_value())
257
+ {
258
+ newState.valid_actions_cache_ = this->valid_actions_cache_;
259
+ }
260
+ else
261
+ {
262
+ newState.valid_actions_cache_ = std::nullopt;
263
+ }
264
+ return newState;
265
+ }
266
+
267
+ void GameStateCpp::debug_toggle_cell(int r, int c)
268
+ {
269
+ if (grid_data_.is_valid(r, c) && !grid_data_.is_death(r, c))
270
+ {
271
+ auto &occupied_grid = grid_data_.get_occupied_grid_mut();
272
+ auto &color_grid = grid_data_.get_color_id_grid_mut();
273
+ bool was_occupied = occupied_grid[r][c];
274
+ occupied_grid[r][c] = !was_occupied;
275
+ color_grid[r][c] = was_occupied ? NO_COLOR_ID : DEBUG_COLOR_ID;
276
+ if (!was_occupied)
277
+ {
278
+ grid_logic::check_and_clear_lines(grid_data_, {{r, c}});
279
+ }
280
+ invalidate_action_cache();
281
+ }
282
+ }
283
+
284
+ void GameStateCpp::debug_set_shapes(const std::vector<std::optional<ShapeCpp>> &new_shapes)
285
+ {
286
+ // Overwrite the current shapes with the provided ones.
287
+ // Ensure the size matches the number of slots.
288
+ size_t num_to_copy = std::min(new_shapes.size(), shapes_.size());
289
+ for (size_t i = 0; i < num_to_copy; ++i)
290
+ {
291
+ shapes_[i] = new_shapes[i];
292
+ }
293
+ // Fill remaining slots with nullopt if new_shapes is shorter
294
+ for (size_t i = num_to_copy; i < shapes_.size(); ++i)
295
+ {
296
+ shapes_[i] = std::nullopt;
297
+ }
298
+ // Invalidate cache as valid actions will change
299
+ invalidate_action_cache();
300
+ }
301
+
302
+ Action GameStateCpp::encode_action(int shape_idx, int r, int c) const
303
+ {
304
+ int grid_size = config_.rows * config_.cols;
305
+ if (shape_idx < 0 || shape_idx >= config_.num_shape_slots || r < 0 || r >= config_.rows || c < 0 || c >= config_.cols)
306
+ {
307
+ throw std::out_of_range("encode_action arguments out of range.");
308
+ }
309
+ return shape_idx * grid_size + r * config_.cols + c;
310
+ }
311
+
312
+ std::tuple<int, int, int> GameStateCpp::decode_action(Action action) const
313
+ {
314
+ int action_dim = config_.num_shape_slots * config_.rows * config_.cols;
315
+ if (action < 0 || action >= action_dim)
316
+ {
317
+ throw std::out_of_range("Action index out of range: " + std::to_string(action));
318
+ }
319
+ int grid_size = config_.rows * config_.cols;
320
+ int shape_idx = action / grid_size;
321
+ int remainder = action % grid_size;
322
+ int r = remainder / config_.cols;
323
+ int c = remainder % config_.cols;
324
+ return {shape_idx, r, c};
325
+ }
326
+
327
+ } // namespace trianglengin::cpp
@@ -0,0 +1,73 @@
1
+ // File: src/trianglengin/cpp/game_state.h
2
+ #ifndef TRIANGLENGIN_CPP_GAME_STATE_H
3
+ #define TRIANGLENGIN_CPP_GAME_STATE_H
4
+
5
+ #pragma once
6
+
7
+ #include <vector>
8
+ #include <set>
9
+ #include <optional>
10
+ #include <random>
11
+ #include <memory>
12
+ #include <string> // Include string for optional<string>
13
+ #include <tuple> // Include tuple for std::tuple
14
+
15
+ #include "config.h"
16
+ #include "structs.h"
17
+ #include "grid_data.h"
18
+ // Remove direct includes causing cycles if possible, use forward declarations
19
+ // #include "grid_logic.h" // Included by game_state.cpp
20
+ // #include "shape_logic.h" // Included by game_state.cpp
21
+
22
+ namespace trianglengin::cpp
23
+ {
24
+
25
+ class GameStateCpp
26
+ {
27
+ public:
28
+ explicit GameStateCpp(const EnvConfigCpp &config, unsigned int initial_seed);
29
+
30
+ void reset();
31
+ std::tuple<double, bool> step(Action action);
32
+ bool is_over() const;
33
+ double get_score() const;
34
+ const std::set<Action> &get_valid_actions(bool force_recalculate = false);
35
+ int get_current_step() const;
36
+ std::optional<std::string> get_game_over_reason() const;
37
+ GameStateCpp copy() const;
38
+ void debug_toggle_cell(int r, int c);
39
+ void invalidate_action_cache(); // Moved to public
40
+ // Debug method to force shapes into slots
41
+ void debug_set_shapes(const std::vector<std::optional<ShapeCpp>> &new_shapes);
42
+
43
+ // Accessors needed by logic functions or bindings
44
+ const GridData &get_grid_data() const { return grid_data_; }
45
+ GridData &get_grid_data_mut() { return grid_data_; }
46
+ const std::vector<std::optional<ShapeCpp>> &get_shapes() const { return shapes_; }
47
+ std::vector<std::optional<ShapeCpp>> &get_shapes_mut() { return shapes_; }
48
+ const EnvConfigCpp &get_config() const { return config_; }
49
+
50
+ private:
51
+ EnvConfigCpp config_;
52
+ GridData grid_data_;
53
+ std::vector<std::optional<ShapeCpp>> shapes_;
54
+ double score_;
55
+ int current_step_;
56
+ bool game_over_;
57
+ std::optional<std::string> game_over_reason_;
58
+ mutable std::optional<std::set<Action>> valid_actions_cache_; // Mutable for const getter
59
+ std::mt19937 rng_;
60
+
61
+ void check_initial_state_game_over();
62
+ void force_game_over(const std::string &reason);
63
+ // void invalidate_action_cache(); // Moved from private
64
+ void calculate_valid_actions_internal() const; // Made const
65
+
66
+ // Action encoding/decoding (can be private if only used internally)
67
+ Action encode_action(int shape_idx, int r, int c) const;
68
+ std::tuple<int, int, int> decode_action(Action action) const;
69
+ };
70
+
71
+ } // namespace trianglengin::cpp
72
+
73
+ #endif // TRIANGLENGIN_CPP_GAME_STATE_H
@@ -0,0 +1,239 @@
1
+ // File: src/trianglengin/cpp/grid_data.cpp
2
+ #include "grid_data.h"
3
+ #include <stdexcept>
4
+ #include <set>
5
+ #include <algorithm>
6
+ #include <iostream>
7
+
8
+ namespace trianglengin::cpp
9
+ {
10
+
11
+ // Constructor: Initialize config_ by copying the passed config
12
+ GridData::GridData(const EnvConfigCpp &config)
13
+ : config_(config), // Copy config here
14
+ rows_(config_.rows),
15
+ cols_(config_.cols)
16
+ {
17
+ if (rows_ <= 0 || cols_ <= 0)
18
+ {
19
+ throw std::invalid_argument("Grid dimensions must be positive.");
20
+ }
21
+ occupied_grid_.assign(rows_, std::vector<bool>(cols_, false));
22
+ color_id_grid_.assign(rows_, std::vector<int8_t>(cols_, NO_COLOR_ID));
23
+ death_grid_.assign(rows_, std::vector<bool>(cols_, true));
24
+
25
+ if (config_.playable_range_per_row.size() != static_cast<size_t>(rows_))
26
+ {
27
+ throw std::invalid_argument("Playable range size mismatch with rows.");
28
+ }
29
+ for (int r = 0; r < rows_; ++r)
30
+ {
31
+ const auto &[start_col, end_col] = config_.playable_range_per_row[r];
32
+ if (start_col < 0 || end_col > cols_ || start_col > end_col) // Allow start == end
33
+ {
34
+ throw std::invalid_argument("Invalid playable range for row " + std::to_string(r));
35
+ }
36
+ if (start_col < end_col)
37
+ {
38
+ for (int c = start_col; c < end_col; ++c)
39
+ {
40
+ death_grid_[r][c] = false;
41
+ }
42
+ }
43
+ }
44
+ precompute_lines();
45
+ }
46
+
47
+ // Copy constructor: Copy the config_ member
48
+ GridData::GridData(const GridData &other)
49
+ : config_(other.config_), // Copy config value
50
+ rows_(other.rows_),
51
+ cols_(other.cols_),
52
+ occupied_grid_(other.occupied_grid_),
53
+ color_id_grid_(other.color_id_grid_),
54
+ death_grid_(other.death_grid_),
55
+ lines_(other.lines_),
56
+ coord_to_lines_map_(other.coord_to_lines_map_)
57
+ {
58
+ }
59
+
60
+ // Copy assignment operator: Copy the config_ member
61
+ GridData &GridData::operator=(const GridData &other)
62
+ {
63
+ if (this != &other)
64
+ {
65
+ config_ = other.config_; // Copy config value
66
+ rows_ = other.rows_;
67
+ cols_ = other.cols_;
68
+ occupied_grid_ = other.occupied_grid_;
69
+ color_id_grid_ = other.color_id_grid_;
70
+ death_grid_ = other.death_grid_;
71
+ lines_ = other.lines_;
72
+ coord_to_lines_map_ = other.coord_to_lines_map_;
73
+ }
74
+ return *this;
75
+ }
76
+
77
+ void GridData::reset()
78
+ {
79
+ for (int r = 0; r < rows_; ++r)
80
+ {
81
+ std::fill(occupied_grid_[r].begin(), occupied_grid_[r].end(), false);
82
+ std::fill(color_id_grid_[r].begin(), color_id_grid_[r].end(), NO_COLOR_ID);
83
+ }
84
+ }
85
+
86
+ bool GridData::is_valid(int r, int c) const
87
+ {
88
+ return r >= 0 && r < rows_ && c >= 0 && c < cols_;
89
+ }
90
+
91
+ bool GridData::is_death(int r, int c) const
92
+ {
93
+ if (!is_valid(r, c))
94
+ {
95
+ throw std::out_of_range("Coordinates (" + std::to_string(r) + "," + std::to_string(c) + ") out of bounds.");
96
+ }
97
+ return death_grid_[r][c];
98
+ }
99
+
100
+ bool GridData::is_occupied(int r, int c) const
101
+ {
102
+ if (!is_valid(r, c))
103
+ {
104
+ throw std::out_of_range("Coordinates (" + std::to_string(r) + "," + std::to_string(c) + ") out of bounds.");
105
+ }
106
+ return !death_grid_[r][c] && occupied_grid_[r][c];
107
+ }
108
+
109
+ std::optional<int> GridData::get_color_id(int r, int c) const
110
+ {
111
+ if (!is_valid(r, c) || death_grid_[r][c] || !occupied_grid_[r][c])
112
+ {
113
+ return std::nullopt;
114
+ }
115
+ return color_id_grid_[r][c];
116
+ }
117
+
118
+ bool GridData::is_up(int r, int c) const
119
+ {
120
+ return (r + c) % 2 != 0;
121
+ }
122
+
123
+ bool GridData::is_live(int r, int c) const
124
+ {
125
+ return is_valid(r, c) && !death_grid_[r][c];
126
+ }
127
+
128
+ std::optional<Coord> GridData::get_neighbor(int r, int c, const std::string &direction, bool backward) const
129
+ {
130
+ bool up = is_up(r, c);
131
+ int nr = -1, nc = -1;
132
+
133
+ if (direction == "h")
134
+ {
135
+ int dc = backward ? -1 : 1;
136
+ nr = r;
137
+ nc = c + dc;
138
+ }
139
+ else if (direction == "d1")
140
+ { // TL-BR
141
+ if (backward)
142
+ {
143
+ nr = up ? r : r - 1;
144
+ nc = up ? c - 1 : c;
145
+ }
146
+ else
147
+ {
148
+ nr = up ? r + 1 : r;
149
+ nc = up ? c : c + 1;
150
+ }
151
+ }
152
+ else if (direction == "d2")
153
+ { // BL-TR
154
+ if (backward)
155
+ {
156
+ nr = up ? r + 1 : r;
157
+ nc = up ? c : c - 1;
158
+ }
159
+ else
160
+ {
161
+ nr = up ? r : r - 1;
162
+ nc = up ? c + 1 : c;
163
+ }
164
+ }
165
+ else
166
+ {
167
+ throw std::invalid_argument("Unknown direction: " + direction);
168
+ }
169
+
170
+ if (!is_valid(nr, nc))
171
+ return std::nullopt;
172
+ return Coord{nr, nc};
173
+ }
174
+
175
+ void GridData::precompute_lines()
176
+ {
177
+ lines_.clear();
178
+ coord_to_lines_map_.clear();
179
+ std::set<Line> maximal_lines_set;
180
+ std::set<std::tuple<Coord, std::string>> processed_starts;
181
+ const std::vector<std::string> directions = {"h", "d1", "d2"};
182
+
183
+ for (int r_init = 0; r_init < rows_; ++r_init)
184
+ {
185
+ for (int c_init = 0; c_init < cols_; ++c_init)
186
+ {
187
+ if (!is_live(r_init, c_init))
188
+ continue;
189
+ Coord start_coord = {r_init, c_init};
190
+ for (const auto &direction : directions)
191
+ {
192
+ Coord line_start_coord = start_coord;
193
+ while (true)
194
+ {
195
+ auto prev_coord_opt = get_neighbor(std::get<0>(line_start_coord), std::get<1>(line_start_coord), direction, true);
196
+ if (prev_coord_opt && is_live(std::get<0>(*prev_coord_opt), std::get<1>(*prev_coord_opt)))
197
+ {
198
+ line_start_coord = *prev_coord_opt;
199
+ }
200
+ else
201
+ break;
202
+ }
203
+ if (processed_starts.count({line_start_coord, direction}))
204
+ continue;
205
+ Line current_line;
206
+ std::optional<Coord> trace_coord_opt = line_start_coord;
207
+ while (trace_coord_opt && is_live(std::get<0>(*trace_coord_opt), std::get<1>(*trace_coord_opt)))
208
+ {
209
+ current_line.push_back(*trace_coord_opt);
210
+ trace_coord_opt = get_neighbor(std::get<0>(*trace_coord_opt), std::get<1>(*trace_coord_opt), direction, false);
211
+ }
212
+ if (current_line.size() >= 2)
213
+ {
214
+ maximal_lines_set.insert(current_line);
215
+ processed_starts.insert({line_start_coord, direction});
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ lines_ = std::vector<Line>(maximal_lines_set.begin(), maximal_lines_set.end());
222
+ std::sort(lines_.begin(), lines_.end(), [](const Line &a, const Line &b)
223
+ {
224
+ if (a.empty() || b.empty()) return b.empty();
225
+ if (std::get<0>(a[0]) != std::get<0>(b[0])) return std::get<0>(a[0]) < std::get<0>(b[0]);
226
+ if (std::get<1>(a[0]) != std::get<1>(b[0])) return std::get<1>(a[0]) < std::get<1>(b[0]);
227
+ return a.size() < b.size(); });
228
+
229
+ for (const auto &line_vec : lines_)
230
+ {
231
+ LineFs line_fs(line_vec.begin(), line_vec.end());
232
+ for (const auto &coord : line_vec)
233
+ {
234
+ coord_to_lines_map_[coord].insert(line_fs);
235
+ }
236
+ }
237
+ }
238
+
239
+ } // namespace trianglengin::cpp