trianglengin 2.0.3__cp312-cp312-macosx_11_0_arm64.whl → 2.0.6__cp312-cp312-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.
@@ -4,7 +4,7 @@
4
4
  #include "shape_logic.h"
5
5
  #include <stdexcept>
6
6
  #include <numeric>
7
- #include <iostream> // <--- Add iostream
7
+ #include <iostream> // Keep iostream if other debug logs might be added later, or remove if not needed
8
8
  #include <algorithm> // For std::min
9
9
 
10
10
  namespace trianglengin::cpp
@@ -12,7 +12,7 @@ namespace trianglengin::cpp
12
12
 
13
13
  GameStateCpp::GameStateCpp(const EnvConfigCpp &config, unsigned int initial_seed)
14
14
  : config_(config),
15
- grid_data_(config_),
15
+ grid_data_(config_), // Initialize GridData with config
16
16
  shapes_(config_.num_shape_slots),
17
17
  score_(0.0),
18
18
  current_step_(0),
@@ -22,6 +22,39 @@ namespace trianglengin::cpp
22
22
  reset();
23
23
  }
24
24
 
25
+ // --- Explicit Copy Constructor ---
26
+ GameStateCpp::GameStateCpp(const GameStateCpp &other)
27
+ : config_(other.config_), // Copy config
28
+ grid_data_(other.grid_data_), // Use GridData's copy constructor
29
+ shapes_(other.shapes_), // Copy vector of optional shapes
30
+ score_(other.score_), // Copy score
31
+ current_step_(other.current_step_), // Copy step
32
+ game_over_(other.game_over_), // Copy game over flag
33
+ game_over_reason_(other.game_over_reason_), // Copy reason
34
+ valid_actions_cache_(other.valid_actions_cache_), // Copy the optional cache
35
+ rng_(other.rng_) // Copy the RNG state
36
+ {
37
+ // No additional logic needed if members handle their own copying well
38
+ }
39
+
40
+ // --- Explicit Copy Assignment Operator ---
41
+ GameStateCpp &GameStateCpp::operator=(const GameStateCpp &other)
42
+ {
43
+ if (this != &other)
44
+ {
45
+ config_ = other.config_;
46
+ grid_data_ = other.grid_data_; // Use GridData's copy assignment
47
+ shapes_ = other.shapes_;
48
+ score_ = other.score_;
49
+ current_step_ = other.current_step_;
50
+ game_over_ = other.game_over_;
51
+ game_over_reason_ = other.game_over_reason_;
52
+ valid_actions_cache_ = other.valid_actions_cache_; // Copy the optional cache
53
+ rng_ = other.rng_; // Copy the RNG state
54
+ }
55
+ return *this;
56
+ }
57
+
25
58
  void GameStateCpp::reset()
26
59
  {
27
60
  grid_data_.reset();
@@ -37,13 +70,9 @@ namespace trianglengin::cpp
37
70
 
38
71
  void GameStateCpp::check_initial_state_game_over()
39
72
  {
40
- get_valid_actions(true); // This calls calculate_valid_actions_internal
41
- if (!game_over_ && valid_actions_cache_ && valid_actions_cache_->empty())
42
- {
43
- force_game_over("No valid actions available at start.");
44
- // Log the reason for immediate game over
45
- std::cerr << "[GameStateCpp::check_initial_state_game_over] Forced game over: No valid actions at start." << std::endl;
46
- }
73
+ // Force calculation which updates the cache and potentially the game_over flag
74
+ get_valid_actions(true);
75
+ // No need to check cache emptiness here, get_valid_actions handles setting the flag
47
76
  }
48
77
 
49
78
  std::tuple<double, bool> GameStateCpp::step(Action action)
@@ -53,14 +82,16 @@ namespace trianglengin::cpp
53
82
  return {0.0, true};
54
83
  }
55
84
 
56
- const auto &valid_actions = get_valid_actions();
85
+ // Ensure cache is populated before checking the action
86
+ const auto &valid_actions = get_valid_actions(); // This populates cache if needed
87
+
88
+ // Check against the (now guaranteed) populated cache
57
89
  if (valid_actions.find(action) == valid_actions.end())
58
90
  {
59
91
  // Invalid action detected
60
92
  force_game_over("Invalid action provided: " + std::to_string(action));
61
- // Apply penalty to score *before* returning
62
- score_ += config_.penalty_game_over;
63
- return {config_.penalty_game_over, true}; // Return penalty and done=true
93
+ score_ += config_.penalty_game_over; // Apply penalty
94
+ return {config_.penalty_game_over, true};
64
95
  }
65
96
 
66
97
  int shape_idx, r, c;
@@ -70,27 +101,25 @@ namespace trianglengin::cpp
70
101
  }
71
102
  catch (const std::out_of_range &e)
72
103
  {
73
- // Error during decoding (should be caught by valid_actions check, but defensive)
74
104
  force_game_over("Failed to decode action: " + std::to_string(action));
75
- score_ += config_.penalty_game_over; // Apply penalty
105
+ score_ += config_.penalty_game_over;
76
106
  return {config_.penalty_game_over, true};
77
107
  }
78
108
 
79
109
  if (shape_idx < 0 || shape_idx >= static_cast<int>(shapes_.size()) || !shapes_[shape_idx].has_value())
80
110
  {
81
- // Action references invalid slot (should be caught by valid_actions check)
82
111
  force_game_over("Action references invalid/empty shape slot: " + std::to_string(shape_idx));
83
- score_ += config_.penalty_game_over; // Apply penalty
112
+ score_ += config_.penalty_game_over;
84
113
  return {config_.penalty_game_over, true};
85
114
  }
86
115
 
87
116
  const ShapeCpp &shape_to_place = shapes_[shape_idx].value();
88
117
 
118
+ // Re-check placement just before modification (defensive)
89
119
  if (!grid_logic::can_place(grid_data_, shape_to_place, r, c))
90
120
  {
91
- // Should not happen if valid_actions is correct
92
121
  force_game_over("Placement check failed for valid action (logic error?). Action: " + std::to_string(action));
93
- score_ += config_.penalty_game_over; // Apply penalty
122
+ score_ += config_.penalty_game_over;
94
123
  return {config_.penalty_game_over, true};
95
124
  }
96
125
 
@@ -106,10 +135,11 @@ namespace trianglengin::cpp
106
135
  std::tie(dr, dc, is_up_ignored) = tri_data;
107
136
  int target_r = r + dr;
108
137
  int target_c = c + dc;
138
+ // Bounds check should be implicitly handled by can_place, but double-check
109
139
  if (!grid_data_.is_valid(target_r, target_c) || grid_data_.is_death(target_r, target_c))
110
140
  {
111
141
  force_game_over("Attempted placement out of bounds/death zone during execution. Action: " + std::to_string(action));
112
- score_ += config_.penalty_game_over; // Apply penalty
142
+ score_ += config_.penalty_game_over;
113
143
  return {config_.penalty_game_over, true};
114
144
  }
115
145
  occupied_grid[target_r][target_c] = true;
@@ -117,7 +147,7 @@ namespace trianglengin::cpp
117
147
  newly_occupied_coords.insert({target_r, target_c});
118
148
  placed_count++;
119
149
  }
120
- shapes_[shape_idx] = std::nullopt;
150
+ shapes_[shape_idx] = std::nullopt; // Clear the used shape slot
121
151
 
122
152
  // --- Line Clearing ---
123
153
  int lines_cleared_count;
@@ -144,9 +174,8 @@ namespace trianglengin::cpp
144
174
 
145
175
  // --- Update State & Check Game Over ---
146
176
  current_step_++;
147
- valid_actions_cache_ = std::nullopt;
148
- get_valid_actions(true); // Force recalculation and update game_over_ if needed
149
- // game_over_ flag is now definitive
177
+ invalidate_action_cache(); // Invalidate cache AFTER state changes
178
+ get_valid_actions(true); // Force recalculation AND update game_over_ flag if needed
150
179
 
151
180
  // --- Calculate Reward & Update Score ---
152
181
  double reward = 0.0;
@@ -155,7 +184,8 @@ namespace trianglengin::cpp
155
184
 
156
185
  if (game_over_)
157
186
  {
158
- reward += config_.penalty_game_over;
187
+ // Penalty was applied earlier if game over was due to invalid action this step.
188
+ // If game over is due to lack of actions *after* this step, no penalty applies here.
159
189
  }
160
190
  else
161
191
  {
@@ -163,34 +193,24 @@ namespace trianglengin::cpp
163
193
  }
164
194
  score_ += reward; // Update score based on calculated reward
165
195
 
166
- return {reward, game_over_};
196
+ return {reward, game_over_}; // Return the potentially updated game_over_ flag
167
197
  }
168
198
 
199
+ // --- Simplified is_over ---
169
200
  bool GameStateCpp::is_over() const
170
201
  {
171
- if (game_over_)
172
- return true;
173
- // If cache exists, use it. Otherwise, calculate.
174
- if (valid_actions_cache_.has_value())
175
- return valid_actions_cache_->empty();
176
- // Need to remove const to calculate and potentially set game_over_
177
- const_cast<GameStateCpp *>(this)->calculate_valid_actions_internal();
178
- // Check game_over_ again as calculate might have set it
179
- if (game_over_)
180
- return true;
181
- // Now the cache should exist
182
- return valid_actions_cache_->empty();
202
+ // The game_over_ flag is the single source of truth.
203
+ // It's updated by step() [via get_valid_actions()] and reset().
204
+ return game_over_;
183
205
  }
184
206
 
185
207
  void GameStateCpp::force_game_over(const std::string &reason)
186
208
  {
187
- // This function only sets the flags and reason.
188
- // Score update should happen in the context where the game over is triggered (e.g., step).
189
209
  if (!game_over_)
190
210
  {
191
211
  game_over_ = true;
192
212
  game_over_reason_ = reason;
193
- valid_actions_cache_ = std::set<Action>();
213
+ valid_actions_cache_ = std::set<Action>(); // Clear valid actions on game over
194
214
  }
195
215
  }
196
216
 
@@ -201,26 +221,34 @@ namespace trianglengin::cpp
201
221
 
202
222
  const std::set<Action> &GameStateCpp::get_valid_actions(bool force_recalculate)
203
223
  {
224
+ // If game is over, always return the cached empty set
204
225
  if (game_over_)
205
226
  {
227
+ // Ensure cache is empty if game_over is true
206
228
  if (!valid_actions_cache_.has_value() || !valid_actions_cache_->empty())
207
229
  {
208
230
  valid_actions_cache_ = std::set<Action>();
209
231
  }
210
232
  return *valid_actions_cache_;
211
233
  }
234
+
235
+ // If not forcing and cache exists, return it
212
236
  if (!force_recalculate && valid_actions_cache_.has_value())
213
237
  {
214
238
  return *valid_actions_cache_;
215
239
  }
240
+
241
+ // Otherwise, calculate (which updates the mutable cache)
216
242
  calculate_valid_actions_internal();
217
- // Check if game should end *after* calculating actions
243
+
244
+ // Check if the calculation resulted in no valid actions, triggering game over
218
245
  if (!game_over_ && valid_actions_cache_->empty())
219
246
  {
247
+ // Set the game over flag HERE, as this is the definitive check after calculation
220
248
  force_game_over("No valid actions available.");
221
- // Log the reason for game over after calculation
222
- std::cerr << "[GameStateCpp::get_valid_actions] Forced game over: No valid actions found after calculation." << std::endl;
223
249
  }
250
+
251
+ // Return the calculated (and potentially now empty) cache
224
252
  return *valid_actions_cache_;
225
253
  }
226
254
 
@@ -229,20 +257,12 @@ namespace trianglengin::cpp
229
257
  valid_actions_cache_ = std::nullopt;
230
258
  }
231
259
 
232
- // Make this non-const so it can modify the cache
233
- void GameStateCpp::calculate_valid_actions_internal() const // Keep const for now, use mutable cache
260
+ // calculate_valid_actions_internal remains const, modifies mutable cache
261
+ void GameStateCpp::calculate_valid_actions_internal() const
234
262
  {
235
- if (game_over_)
236
- {
237
- valid_actions_cache_ = std::set<Action>();
238
- // Add logging for this case too
239
- std::cerr << "[GameStateCpp::calculate_valid_actions_internal] Game already over. Returning empty set." << std::endl;
240
- return;
241
- }
263
+ // This function should NOT set game_over_ directly.
264
+ // It just calculates the set. The caller (get_valid_actions) checks if empty.
242
265
  std::set<Action> valid_actions;
243
- int can_place_true_count = 0; // Counter for debugging
244
- int attempts_count = 0; // Counter for total placement checks
245
-
246
266
  for (int shape_idx = 0; shape_idx < static_cast<int>(shapes_.size()); ++shape_idx)
247
267
  {
248
268
  if (!shapes_[shape_idx].has_value())
@@ -250,50 +270,26 @@ namespace trianglengin::cpp
250
270
  const ShapeCpp &shape = shapes_[shape_idx].value();
251
271
  for (int r = 0; r < config_.rows; ++r)
252
272
  {
253
- const auto &[start_c, end_c] = config_.playable_range_per_row[r];
254
- for (int c = start_c; c < end_c; ++c)
273
+ // Optimization: Check only within playable range? No, C++ can_place handles death zones.
274
+ for (int c = 0; c < config_.cols; ++c)
255
275
  {
256
- attempts_count++; // Increment attempt counter
257
- bool can_place_result = grid_logic::can_place(grid_data_, shape, r, c); // Store result
258
- if (can_place_result)
276
+ if (grid_logic::can_place(grid_data_, shape, r, c))
259
277
  {
260
278
  valid_actions.insert(encode_action(shape_idx, r, c));
261
- can_place_true_count++; // Increment counter
262
279
  }
263
- // Optional: Log failed attempts if needed for deep debugging
264
- // else {
265
- // std::cerr << "[Debug] can_place failed for shape " << shape_idx << " at (" << r << "," << c << ")" << std::endl;
266
- // }
267
280
  }
268
281
  }
269
282
  }
270
- // Add logging here
271
- std::cerr << "[GameStateCpp::calculate_valid_actions_internal] Step: " << current_step_
272
- << ", Attempts: " << attempts_count
273
- << ", CanPlaceTrue: " << can_place_true_count
274
- << ", ValidActionsFound: " << valid_actions.size()
275
- << std::endl;
276
-
277
- // Use mutable cache
278
- valid_actions_cache_ = std::move(valid_actions);
283
+ valid_actions_cache_ = std::move(valid_actions); // Update the mutable cache
279
284
  }
280
285
 
281
286
  int GameStateCpp::get_current_step() const { return current_step_; }
282
287
  std::optional<std::string> GameStateCpp::get_game_over_reason() const { return game_over_reason_; }
283
288
 
289
+ // Python-facing copy method uses the C++ copy constructor
284
290
  GameStateCpp GameStateCpp::copy() const
285
291
  {
286
- GameStateCpp newState = *this;
287
- // Copy the cache state explicitly
288
- if (this->valid_actions_cache_.has_value())
289
- {
290
- newState.valid_actions_cache_ = this->valid_actions_cache_;
291
- }
292
- else
293
- {
294
- newState.valid_actions_cache_ = std::nullopt;
295
- }
296
- return newState;
292
+ return GameStateCpp(*this);
297
293
  }
298
294
 
299
295
  void GameStateCpp::debug_toggle_cell(int r, int c)
@@ -311,33 +307,36 @@ namespace trianglengin::cpp
311
307
  grid_logic::check_and_clear_lines(grid_data_, {{r, c}});
312
308
  }
313
309
  invalidate_action_cache(); // Always invalidate after manual change
310
+ // Force recalculation of valid actions and game over state after toggle
311
+ get_valid_actions(true);
314
312
  }
315
313
  }
316
314
 
317
315
  void GameStateCpp::debug_set_shapes(const std::vector<std::optional<ShapeCpp>> &new_shapes)
318
316
  {
319
- // Overwrite the current shapes with the provided ones.
320
- // Ensure the size matches the number of slots.
321
317
  size_t num_to_copy = std::min(new_shapes.size(), shapes_.size());
322
318
  for (size_t i = 0; i < num_to_copy; ++i)
323
319
  {
324
320
  shapes_[i] = new_shapes[i];
325
321
  }
326
- // Fill remaining slots with nullopt if new_shapes is shorter
327
322
  for (size_t i = num_to_copy; i < shapes_.size(); ++i)
328
323
  {
329
324
  shapes_[i] = std::nullopt;
330
325
  }
331
- // Invalidate cache as valid actions will change
332
326
  invalidate_action_cache();
327
+ // Force recalculation of valid actions and game over state after setting shapes
328
+ get_valid_actions(true);
333
329
  }
334
330
 
335
331
  Action GameStateCpp::encode_action(int shape_idx, int r, int c) const
336
332
  {
337
333
  int grid_size = config_.rows * config_.cols;
334
+ // Basic bounds check (more robust check in can_place)
338
335
  if (shape_idx < 0 || shape_idx >= config_.num_shape_slots || r < 0 || r >= config_.rows || c < 0 || c >= config_.cols)
339
336
  {
340
- throw std::out_of_range("encode_action arguments out of range.");
337
+ // This case should ideally not be reached if called after can_place
338
+ // Return an invalid action index or throw? Let's throw for internal logic errors.
339
+ throw std::out_of_range("encode_action arguments out of range during valid action calculation.");
341
340
  }
342
341
  return shape_idx * grid_size + r * config_.cols + c;
343
342
  }
@@ -27,6 +27,13 @@ namespace trianglengin::cpp
27
27
  public:
28
28
  explicit GameStateCpp(const EnvConfigCpp &config, unsigned int initial_seed);
29
29
 
30
+ // --- Rule of 5 ---
31
+ ~GameStateCpp() = default; // Default destructor
32
+ GameStateCpp(const GameStateCpp &other); // Copy constructor
33
+ GameStateCpp &operator=(const GameStateCpp &other); // Copy assignment operator
34
+ GameStateCpp(GameStateCpp &&other) noexcept = default; // Default move constructor
35
+ GameStateCpp &operator=(GameStateCpp &&other) noexcept = default; // Default move assignment operator
36
+
30
37
  void reset();
31
38
  std::tuple<double, bool> step(Action action);
32
39
  bool is_over() const;
@@ -34,7 +41,7 @@ namespace trianglengin::cpp
34
41
  const std::set<Action> &get_valid_actions(bool force_recalculate = false);
35
42
  int get_current_step() const;
36
43
  std::optional<std::string> get_game_over_reason() const;
37
- GameStateCpp copy() const;
44
+ GameStateCpp copy() const; // Keep Python-facing copy method
38
45
  void debug_toggle_cell(int r, int c);
39
46
  void invalidate_action_cache(); // Moved to public
40
47
  // Debug method to force shapes into slots
@@ -46,6 +53,8 @@ namespace trianglengin::cpp
46
53
  const std::vector<std::optional<ShapeCpp>> &get_shapes() const { return shapes_; }
47
54
  std::vector<std::optional<ShapeCpp>> &get_shapes_mut() { return shapes_; }
48
55
  const EnvConfigCpp &get_config() const { return config_; }
56
+ // Expose RNG state for copying if needed (or handle seeding in copy)
57
+ std::mt19937 get_rng_state() const { return rng_; }
49
58
 
50
59
  private:
51
60
  EnvConfigCpp config_;
@@ -44,7 +44,7 @@ namespace trianglengin::cpp
44
44
  precompute_lines();
45
45
  }
46
46
 
47
- // Copy constructor: Copy the config_ member
47
+ // Copy constructor: Explicitly copy all members
48
48
  GridData::GridData(const GridData &other)
49
49
  : config_(other.config_), // Copy config value
50
50
  rows_(other.rows_),
@@ -55,9 +55,11 @@ namespace trianglengin::cpp
55
55
  lines_(other.lines_),
56
56
  coord_to_lines_map_(other.coord_to_lines_map_)
57
57
  {
58
+ // All members are copyable, default member-wise copy is sufficient here,
59
+ // but being explicit ensures correctness if members change later.
58
60
  }
59
61
 
60
- // Copy assignment operator: Copy the config_ member
62
+ // Copy assignment operator: Explicitly copy all members
61
63
  GridData &GridData::operator=(const GridData &other)
62
64
  {
63
65
  if (this != &other)
@@ -103,7 +105,8 @@ namespace trianglengin::cpp
103
105
  {
104
106
  throw std::out_of_range("Coordinates (" + std::to_string(r) + "," + std::to_string(c) + ") out of bounds.");
105
107
  }
106
- return !death_grid_[r][c] && occupied_grid_[r][c];
108
+ // An occupied cell cannot be a death cell by game logic after placement/clearing
109
+ return occupied_grid_[r][c];
107
110
  }
108
111
 
109
112
  std::optional<int> GridData::get_color_id(int r, int c) const
@@ -190,6 +193,7 @@ namespace trianglengin::cpp
190
193
  for (const auto &direction : directions)
191
194
  {
192
195
  Coord line_start_coord = start_coord;
196
+ // Find the true start of the line segment in this direction
193
197
  while (true)
194
198
  {
195
199
  auto prev_coord_opt = get_neighbor(std::get<0>(line_start_coord), std::get<1>(line_start_coord), direction, true);
@@ -200,8 +204,11 @@ namespace trianglengin::cpp
200
204
  else
201
205
  break;
202
206
  }
207
+ // Check if we already processed this line starting from this coordinate and direction
203
208
  if (processed_starts.count({line_start_coord, direction}))
204
209
  continue;
210
+
211
+ // Trace the line forward from the true start
205
212
  Line current_line;
206
213
  std::optional<Coord> trace_coord_opt = line_start_coord;
207
214
  while (trace_coord_opt && is_live(std::get<0>(*trace_coord_opt), std::get<1>(*trace_coord_opt)))
@@ -209,7 +216,9 @@ namespace trianglengin::cpp
209
216
  current_line.push_back(*trace_coord_opt);
210
217
  trace_coord_opt = get_neighbor(std::get<0>(*trace_coord_opt), std::get<1>(*trace_coord_opt), direction, false);
211
218
  }
212
- if (current_line.size() >= 2)
219
+
220
+ // Store the line if it's long enough and mark it as processed
221
+ if (current_line.size() >= 2) // Only store lines of length 2 or more
213
222
  {
214
223
  maximal_lines_set.insert(current_line);
215
224
  processed_starts.insert({line_start_coord, direction});
@@ -218,16 +227,20 @@ namespace trianglengin::cpp
218
227
  }
219
228
  }
220
229
 
230
+ // Convert set to vector and sort for deterministic order
221
231
  lines_ = std::vector<Line>(maximal_lines_set.begin(), maximal_lines_set.end());
222
232
  std::sort(lines_.begin(), lines_.end(), [](const Line &a, const Line &b)
223
233
  {
224
- if (a.empty() || b.empty()) return b.empty();
234
+ if (a.empty() || b.empty()) return b.empty(); // Handle empty lines if they somehow occur
235
+ // Sort primarily by starting row, then starting column, then size
225
236
  if (std::get<0>(a[0]) != std::get<0>(b[0])) return std::get<0>(a[0]) < std::get<0>(b[0]);
226
237
  if (std::get<1>(a[0]) != std::get<1>(b[0])) return std::get<1>(a[0]) < std::get<1>(b[0]);
227
238
  return a.size() < b.size(); });
228
239
 
240
+ // Build the coordinate-to-lines map
229
241
  for (const auto &line_vec : lines_)
230
242
  {
243
+ // Use a set of Coords (LineFs) as the value in the map for efficient lookup
231
244
  LineFs line_fs(line_vec.begin(), line_vec.end());
232
245
  for (const auto &coord : line_vec)
233
246
  {
@@ -48,10 +48,9 @@ namespace trianglengin::cpp
48
48
  int rows() const { return rows_; }
49
49
  int cols() const { return cols_; }
50
50
 
51
- // Copy constructor
52
- GridData(const GridData &other);
53
- // Copy assignment operator
54
- GridData &operator=(const GridData &other);
51
+ // --- Explicit Copy Control ---
52
+ GridData(const GridData &other); // Copy constructor
53
+ GridData &operator=(const GridData &other); // Copy assignment operator
55
54
 
56
55
  // Default move constructor/assignment should work now
57
56
  GridData(GridData &&other) noexcept = default;
@@ -75,4 +74,4 @@ namespace trianglengin::cpp
75
74
 
76
75
  } // namespace trianglengin::cpp
77
76
 
78
- #endif // TRIANGLENGIN_CPP_GRID_DATA_H
77
+ #endif // TRIANGLENGIN_CPP_GRID_DATA_H
@@ -8,7 +8,9 @@
8
8
  #include <tuple>
9
9
  #include <string>
10
10
  #include <cstdint>
11
- #include <utility> // For std::move
11
+ #include <utility> // For std::move
12
+ #include <optional> // For optional members
13
+ #include <set> // For previous valid actions
12
14
 
13
15
  namespace trianglengin::cpp
14
16
  {
@@ -35,6 +37,33 @@ namespace trianglengin::cpp
35
37
  return triangles == other.triangles && color == other.color && color_id == other.color_id;
36
38
  }
37
39
  };
40
+
41
+ // --- NEW: Structure to hold undo information ---
42
+ struct StepUndoInfo
43
+ {
44
+ // Cells changed by placement or clearing
45
+ // Stores (row, col, previous_occupied_state, previous_color_id)
46
+ std::vector<std::tuple<int, int, bool, int8_t>> changed_cells;
47
+
48
+ // Info about the shape that was consumed by the step
49
+ int consumed_shape_slot = -1;
50
+ std::optional<ShapeCpp> consumed_shape = std::nullopt;
51
+
52
+ // Previous game state variables
53
+ double previous_score = 0.0;
54
+ int previous_step = 0;
55
+ bool previous_game_over = false;
56
+ std::optional<std::string> previous_game_over_reason = std::nullopt;
57
+
58
+ // Cache invalidation marker (or potentially the previous cache itself)
59
+ // Storing the previous cache might be large, maybe just invalidate?
60
+ // Let's start by just marking that the cache *was* valid before the step.
61
+ bool was_action_cache_valid = false;
62
+ // OPTIONAL (more complex): Store the actual previous cache
63
+ // std::optional<std::set<Action>> previous_valid_actions_cache = std::nullopt;
64
+ };
65
+ // --- END NEW ---
66
+
38
67
  } // namespace trianglengin::cpp
39
68
 
40
69
  #endif // TRIANGLENGIN_CPP_STRUCTS_H
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trianglengin
3
- Version: 2.0.3
3
+ Version: 2.0.6
4
4
  Summary: High-performance C++/Python engine for a triangle puzzle game.
5
5
  Author-email: "Luis Guilherme P. M." <lgpelin92@gmail.com>
6
6
  License-Expression: MIT
@@ -1,11 +1,5 @@
1
- trianglengin-2.0.3.dist-info/RECORD,,
2
- trianglengin-2.0.3.dist-info/WHEEL,sha256=s3SCZeN-G9VuOPYdbIQ149WhzOgcaPLKGJFMWgov7lM,109
3
- trianglengin-2.0.3.dist-info/entry_points.txt,sha256=kQEqO_U-MEpMEC0xwOPSucBzQIq2Ny7XwCtFSruZhvY,57
4
- trianglengin-2.0.3.dist-info/top_level.txt,sha256=YsSWmp_2zM23wRc5TRERHpVCgQuVYieYHDTpnwVQC7Y,13
5
- trianglengin-2.0.3.dist-info/METADATA,sha256=JQjjqC7xfHqrYAoSZx-07pFzVN4hb1SmyDFPvHG_Lvo,12066
6
- trianglengin-2.0.3.dist-info/licenses/LICENSE,sha256=So3rgoJp-HgoxkclxZLIBC3pmmTwshN4tUO8KiQ6akc,1077
7
1
  trianglengin/__init__.py,sha256=4INnGvOYIjzOyCew7lhX6Irc8_H7QVxWc5jopt9TCZ4,919
8
- trianglengin/trianglengin_cpp.cpython-312-darwin.so,sha256=pMehdBhE13Yze_uuvwAd6gujU7lndTf4HVbSbJazba0,251464
2
+ trianglengin/trianglengin_cpp.cpython-312-darwin.so,sha256=GBfE9mfLIeLuubw7ZBB9cCuPM6chL6gFscimedkhbkQ,233432
9
3
  trianglengin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
4
  trianglengin/game_interface.py,sha256=ijfBH08DU2sHyMH6XZmQbFQZDPGsKlkk3gk3AUt3fmU,9304
11
5
  trianglengin/ui/config.py,sha256=E68Qvbr3dwyhC_Rh_PK-QHWlHOrXFQhAsARZZMoQHNs,1579
@@ -48,11 +42,17 @@ trianglengin/cpp/grid_logic.h,sha256=O6roOE_U7nbKgAkaMkC8LrEjGDvnL3QiVzOnXunXy9s
48
42
  trianglengin/cpp/CMakeLists.txt,sha256=FCUnDHVQhe7PFj9FTQcQiWFgr61b8iJg0m0UH3hGYE4,1328
49
43
  trianglengin/cpp/grid_logic.cpp,sha256=8ag6wwdmZuHg8_I-skTI9LOdB84a82IrZAoREq0dkQo,3333
50
44
  trianglengin/cpp/config.h,sha256=9Xd-9wTYmFwEuf-ZjrQa5zlXF58MrLhRKU1ygyG2Nmc,715
51
- trianglengin/cpp/grid_data.cpp,sha256=KH9BbLuhi-_fmKgnT2oPuzHRDo87xFDHiBfpXCwgYsw,6904
52
- trianglengin/cpp/grid_data.h,sha256=KGOtUQQb7HJnU_LyWIWW5g6S2gJ2u3ewY32qXtoPfcs,2453
53
- trianglengin/cpp/game_state.h,sha256=MBoAiG617XPtZNGqGPE2PC1pX1dBHuWBGHLJr3iBg6c,2547
54
- trianglengin/cpp/game_state.cpp,sha256=WRYA_alYDZs2zR7NU1LMnZ9reKF8YvXGzF7KubQU0ys,12333
55
- trianglengin/cpp/structs.h,sha256=v_aL5O5gwXH_qRPGUIkNplr167rgf7gCVlWIJKOLV1U,993
45
+ trianglengin/cpp/grid_data.cpp,sha256=WDdaYN8GIPHxKMNvxSI_oUWa23pVYRt5WhX0N5EMGoU,7773
46
+ trianglengin/cpp/grid_data.h,sha256=j_VDdBCQELg8m_kJ4GIyHstt7ynyLgjHyTcdBPv_AP4,2492
47
+ trianglengin/cpp/game_state.h,sha256=yhP0Uj1YAZ_eoTU9ZmI9xMTYPmdBYriXXdtwYMWpRR4,3221
48
+ trianglengin/cpp/game_state.cpp,sha256=kvqUndWqV0kGLHW897RJNnFl8zwC8Dor5mGarnKXgLg,12710
49
+ trianglengin/cpp/structs.h,sha256=yGJp_VatwCHpDAcYJBpsIfEMEG4BQuqdDObqd7nBY7M,2155
56
50
  trianglengin/cpp/shape_logic.h,sha256=QvjLAPG5y42FetpXXuCcYrWlcwvKGo7csU8Ch-RmdFs,626
57
51
  trianglengin/cpp/bindings.cpp,sha256=B3hfxIF_pBAyVG1QpbBvJT1awy8uJWRJSBNxaNY5_pY,8689
58
52
  trianglengin/cpp/shape_logic.cpp,sha256=gF1mgolaNvxpNphwDScNUvOkoq4FuxFASB3n50WI3Bo,4133
53
+ trianglengin-2.0.6.dist-info/RECORD,,
54
+ trianglengin-2.0.6.dist-info/WHEEL,sha256=s3SCZeN-G9VuOPYdbIQ149WhzOgcaPLKGJFMWgov7lM,109
55
+ trianglengin-2.0.6.dist-info/entry_points.txt,sha256=kQEqO_U-MEpMEC0xwOPSucBzQIq2Ny7XwCtFSruZhvY,57
56
+ trianglengin-2.0.6.dist-info/top_level.txt,sha256=YsSWmp_2zM23wRc5TRERHpVCgQuVYieYHDTpnwVQC7Y,13
57
+ trianglengin-2.0.6.dist-info/METADATA,sha256=KpwlhUXJZa2uU6il2LnLChKau4twfPHHW-YQDXet7RM,12066
58
+ trianglengin-2.0.6.dist-info/licenses/LICENSE,sha256=So3rgoJp-HgoxkclxZLIBC3pmmTwshN4tUO8KiQ6akc,1077