trianglengin 2.0.6__cp310-cp310-win32.whl → 2.0.7__cp310-cp310-win32.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.
@@ -1,13 +1,14 @@
1
+
1
2
  // File: src/trianglengin/cpp/bindings.cpp
2
3
  #include <pybind11/pybind11.h>
3
4
  #include <pybind11/stl.h>
4
5
  #include <pybind11/operators.h>
5
6
  #include <pybind11/numpy.h>
6
- #include <pybind11/functional.h> // Needed for std::function with optional
7
+ #include <pybind11/functional.h>
7
8
  #include <vector>
8
9
  #include <stdexcept>
9
- #include <cstring> // For memcpy with int8_t
10
- #include <optional> // Include optional
10
+ #include <cstring>
11
+ #include <optional>
11
12
 
12
13
  #include "game_state.h"
13
14
  #include "config.h"
@@ -138,6 +139,7 @@ PYBIND11_MODULE(trianglengin_cpp, m)
138
139
  .def("get_score", &tg::GameStateCpp::get_score)
139
140
  .def("get_valid_actions", &tg::GameStateCpp::get_valid_actions, py::arg("force_recalculate") = false, py::return_value_policy::reference_internal)
140
141
  .def("get_current_step", &tg::GameStateCpp::get_current_step)
142
+ .def("get_last_cleared_triangles", &tg::GameStateCpp::get_last_cleared_triangles) // Added binding
141
143
  .def("get_game_over_reason", &tg::GameStateCpp::get_game_over_reason)
142
144
  .def("get_shapes_cpp", [](const tg::GameStateCpp &gs)
143
145
  {
@@ -154,7 +156,6 @@ PYBIND11_MODULE(trianglengin_cpp, m)
154
156
  py::array_t<bool> result({rows, cols});
155
157
  auto buf = result.request();
156
158
  bool *ptr = static_cast<bool *>(buf.ptr);
157
- // Manual copy for std::vector<bool>
158
159
  for (size_t r = 0; r < rows; ++r) {
159
160
  for (size_t c = 0; c < cols; ++c) {
160
161
  ptr[r * cols + c] = grid[r][c];
@@ -169,7 +170,6 @@ PYBIND11_MODULE(trianglengin_cpp, m)
169
170
  py::array_t<int8_t> result({rows, cols});
170
171
  auto buf = result.request();
171
172
  int8_t *ptr = static_cast<int8_t *>(buf.ptr);
172
- // memcpy is fine for int8_t
173
173
  for (size_t r = 0; r < rows; ++r) {
174
174
  std::memcpy(ptr + r * cols, grid[r].data(), cols * sizeof(int8_t));
175
175
  }
@@ -182,7 +182,6 @@ PYBIND11_MODULE(trianglengin_cpp, m)
182
182
  py::array_t<bool> result({rows, cols});
183
183
  auto buf = result.request();
184
184
  bool *ptr = static_cast<bool *>(buf.ptr);
185
- // Manual copy for std::vector<bool>
186
185
  for (size_t r = 0; r < rows; ++r) {
187
186
  for (size_t c = 0; c < cols; ++c) {
188
187
  ptr[r * cols + c] = grid[r][c];
@@ -191,13 +190,11 @@ PYBIND11_MODULE(trianglengin_cpp, m)
191
190
  return result; })
192
191
  .def("copy", &tg::GameStateCpp::copy)
193
192
  .def("debug_toggle_cell", &tg::GameStateCpp::debug_toggle_cell, py::arg("r"), py::arg("c"))
194
- // Add binding for debug_set_shapes
195
193
  .def("debug_set_shapes", [](tg::GameStateCpp &gs, const py::list &shapes_py)
196
194
  {
197
195
  std::vector<std::optional<tg::ShapeCpp>> shapes_cpp;
198
196
  shapes_cpp.reserve(shapes_py.size());
199
197
  for(const auto& shape_item_handle : shapes_py) {
200
- // Cast handle to object before passing to conversion function
201
198
  py::object shape_item = py::reinterpret_borrow<py::object>(shape_item_handle);
202
199
  shapes_cpp.push_back(python_to_cpp_shape(shape_item));
203
200
  }
@@ -1,10 +1,11 @@
1
+
1
2
  // File: src/trianglengin/cpp/game_state.cpp
2
3
  #include "game_state.h"
3
4
  #include "grid_logic.h"
4
5
  #include "shape_logic.h"
5
6
  #include <stdexcept>
6
7
  #include <numeric>
7
- #include <iostream> // Keep iostream if other debug logs might be added later, or remove if not needed
8
+ #include <iostream>
8
9
  #include <algorithm> // For std::min
9
10
 
10
11
  namespace trianglengin::cpp
@@ -12,10 +13,11 @@ namespace trianglengin::cpp
12
13
 
13
14
  GameStateCpp::GameStateCpp(const EnvConfigCpp &config, unsigned int initial_seed)
14
15
  : config_(config),
15
- grid_data_(config_), // Initialize GridData with config
16
+ grid_data_(config_),
16
17
  shapes_(config_.num_shape_slots),
17
18
  score_(0.0),
18
19
  current_step_(0),
20
+ last_cleared_triangles_(0), // Initialize added member
19
21
  game_over_(false),
20
22
  rng_(initial_seed)
21
23
  {
@@ -24,17 +26,17 @@ namespace trianglengin::cpp
24
26
 
25
27
  // --- Explicit Copy Constructor ---
26
28
  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
29
+ : config_(other.config_),
30
+ grid_data_(other.grid_data_),
31
+ shapes_(other.shapes_),
32
+ score_(other.score_),
33
+ current_step_(other.current_step_),
34
+ last_cleared_triangles_(other.last_cleared_triangles_), // Copy added member
35
+ game_over_(other.game_over_),
36
+ game_over_reason_(other.game_over_reason_),
37
+ valid_actions_cache_(other.valid_actions_cache_),
38
+ rng_(other.rng_)
36
39
  {
37
- // No additional logic needed if members handle their own copying well
38
40
  }
39
41
 
40
42
  // --- Explicit Copy Assignment Operator ---
@@ -43,14 +45,15 @@ namespace trianglengin::cpp
43
45
  if (this != &other)
44
46
  {
45
47
  config_ = other.config_;
46
- grid_data_ = other.grid_data_; // Use GridData's copy assignment
48
+ grid_data_ = other.grid_data_;
47
49
  shapes_ = other.shapes_;
48
50
  score_ = other.score_;
49
51
  current_step_ = other.current_step_;
52
+ last_cleared_triangles_ = other.last_cleared_triangles_; // Copy added member
50
53
  game_over_ = other.game_over_;
51
54
  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
55
+ valid_actions_cache_ = other.valid_actions_cache_;
56
+ rng_ = other.rng_;
54
57
  }
55
58
  return *this;
56
59
  }
@@ -61,6 +64,7 @@ namespace trianglengin::cpp
61
64
  std::fill(shapes_.begin(), shapes_.end(), std::nullopt);
62
65
  score_ = 0.0;
63
66
  current_step_ = 0;
67
+ last_cleared_triangles_ = 0; // Reset added member
64
68
  game_over_ = false;
65
69
  game_over_reason_ = std::nullopt;
66
70
  valid_actions_cache_ = std::nullopt;
@@ -70,27 +74,24 @@ namespace trianglengin::cpp
70
74
 
71
75
  void GameStateCpp::check_initial_state_game_over()
72
76
  {
73
- // Force calculation which updates the cache and potentially the game_over flag
74
77
  get_valid_actions(true);
75
- // No need to check cache emptiness here, get_valid_actions handles setting the flag
76
78
  }
77
79
 
78
80
  std::tuple<double, bool> GameStateCpp::step(Action action)
79
81
  {
82
+ last_cleared_triangles_ = 0; // Reset before potential clearing
83
+
80
84
  if (game_over_)
81
85
  {
82
86
  return {0.0, true};
83
87
  }
84
88
 
85
- // Ensure cache is populated before checking the action
86
- const auto &valid_actions = get_valid_actions(); // This populates cache if needed
89
+ const auto &valid_actions = get_valid_actions();
87
90
 
88
- // Check against the (now guaranteed) populated cache
89
91
  if (valid_actions.find(action) == valid_actions.end())
90
92
  {
91
- // Invalid action detected
92
93
  force_game_over("Invalid action provided: " + std::to_string(action));
93
- score_ += config_.penalty_game_over; // Apply penalty
94
+ score_ += config_.penalty_game_over;
94
95
  return {config_.penalty_game_over, true};
95
96
  }
96
97
 
@@ -115,7 +116,6 @@ namespace trianglengin::cpp
115
116
 
116
117
  const ShapeCpp &shape_to_place = shapes_[shape_idx].value();
117
118
 
118
- // Re-check placement just before modification (defensive)
119
119
  if (!grid_logic::can_place(grid_data_, shape_to_place, r, c))
120
120
  {
121
121
  force_game_over("Placement check failed for valid action (logic error?). Action: " + std::to_string(action));
@@ -135,7 +135,6 @@ namespace trianglengin::cpp
135
135
  std::tie(dr, dc, is_up_ignored) = tri_data;
136
136
  int target_r = r + dr;
137
137
  int target_c = c + dc;
138
- // Bounds check should be implicitly handled by can_place, but double-check
139
138
  if (!grid_data_.is_valid(target_r, target_c) || grid_data_.is_death(target_r, target_c))
140
139
  {
141
140
  force_game_over("Attempted placement out of bounds/death zone during execution. Action: " + std::to_string(action));
@@ -147,7 +146,7 @@ namespace trianglengin::cpp
147
146
  newly_occupied_coords.insert({target_r, target_c});
148
147
  placed_count++;
149
148
  }
150
- shapes_[shape_idx] = std::nullopt; // Clear the used shape slot
149
+ shapes_[shape_idx] = std::nullopt;
151
150
 
152
151
  // --- Line Clearing ---
153
152
  int lines_cleared_count;
@@ -156,6 +155,7 @@ namespace trianglengin::cpp
156
155
  std::tie(lines_cleared_count, cleared_coords, cleared_lines_fs) =
157
156
  grid_logic::check_and_clear_lines(grid_data_, newly_occupied_coords);
158
157
  int cleared_count = static_cast<int>(cleared_coords.size());
158
+ last_cleared_triangles_ = cleared_count; // Store cleared count
159
159
 
160
160
  // --- Refill ---
161
161
  bool all_slots_empty = true;
@@ -174,8 +174,8 @@ namespace trianglengin::cpp
174
174
 
175
175
  // --- Update State & Check Game Over ---
176
176
  current_step_++;
177
- invalidate_action_cache(); // Invalidate cache AFTER state changes
178
- get_valid_actions(true); // Force recalculation AND update game_over_ flag if needed
177
+ invalidate_action_cache();
178
+ get_valid_actions(true);
179
179
 
180
180
  // --- Calculate Reward & Update Score ---
181
181
  double reward = 0.0;
@@ -184,23 +184,19 @@ namespace trianglengin::cpp
184
184
 
185
185
  if (game_over_)
186
186
  {
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.
187
+ // Penalty already applied if game over was due to invalid action this step.
189
188
  }
190
189
  else
191
190
  {
192
191
  reward += config_.reward_per_step_alive;
193
192
  }
194
- score_ += reward; // Update score based on calculated reward
193
+ score_ += reward;
195
194
 
196
- return {reward, game_over_}; // Return the potentially updated game_over_ flag
195
+ return {reward, game_over_};
197
196
  }
198
197
 
199
- // --- Simplified is_over ---
200
198
  bool GameStateCpp::is_over() const
201
199
  {
202
- // The game_over_ flag is the single source of truth.
203
- // It's updated by step() [via get_valid_actions()] and reset().
204
200
  return game_over_;
205
201
  }
206
202
 
@@ -210,7 +206,7 @@ namespace trianglengin::cpp
210
206
  {
211
207
  game_over_ = true;
212
208
  game_over_reason_ = reason;
213
- valid_actions_cache_ = std::set<Action>(); // Clear valid actions on game over
209
+ valid_actions_cache_ = std::set<Action>();
214
210
  }
215
211
  }
216
212
 
@@ -221,10 +217,8 @@ namespace trianglengin::cpp
221
217
 
222
218
  const std::set<Action> &GameStateCpp::get_valid_actions(bool force_recalculate)
223
219
  {
224
- // If game is over, always return the cached empty set
225
220
  if (game_over_)
226
221
  {
227
- // Ensure cache is empty if game_over is true
228
222
  if (!valid_actions_cache_.has_value() || !valid_actions_cache_->empty())
229
223
  {
230
224
  valid_actions_cache_ = std::set<Action>();
@@ -232,23 +226,18 @@ namespace trianglengin::cpp
232
226
  return *valid_actions_cache_;
233
227
  }
234
228
 
235
- // If not forcing and cache exists, return it
236
229
  if (!force_recalculate && valid_actions_cache_.has_value())
237
230
  {
238
231
  return *valid_actions_cache_;
239
232
  }
240
233
 
241
- // Otherwise, calculate (which updates the mutable cache)
242
234
  calculate_valid_actions_internal();
243
235
 
244
- // Check if the calculation resulted in no valid actions, triggering game over
245
236
  if (!game_over_ && valid_actions_cache_->empty())
246
237
  {
247
- // Set the game over flag HERE, as this is the definitive check after calculation
248
238
  force_game_over("No valid actions available.");
249
239
  }
250
240
 
251
- // Return the calculated (and potentially now empty) cache
252
241
  return *valid_actions_cache_;
253
242
  }
254
243
 
@@ -257,11 +246,8 @@ namespace trianglengin::cpp
257
246
  valid_actions_cache_ = std::nullopt;
258
247
  }
259
248
 
260
- // calculate_valid_actions_internal remains const, modifies mutable cache
261
249
  void GameStateCpp::calculate_valid_actions_internal() const
262
250
  {
263
- // This function should NOT set game_over_ directly.
264
- // It just calculates the set. The caller (get_valid_actions) checks if empty.
265
251
  std::set<Action> valid_actions;
266
252
  for (int shape_idx = 0; shape_idx < static_cast<int>(shapes_.size()); ++shape_idx)
267
253
  {
@@ -270,7 +256,6 @@ namespace trianglengin::cpp
270
256
  const ShapeCpp &shape = shapes_[shape_idx].value();
271
257
  for (int r = 0; r < config_.rows; ++r)
272
258
  {
273
- // Optimization: Check only within playable range? No, C++ can_place handles death zones.
274
259
  for (int c = 0; c < config_.cols; ++c)
275
260
  {
276
261
  if (grid_logic::can_place(grid_data_, shape, r, c))
@@ -280,13 +265,13 @@ namespace trianglengin::cpp
280
265
  }
281
266
  }
282
267
  }
283
- valid_actions_cache_ = std::move(valid_actions); // Update the mutable cache
268
+ valid_actions_cache_ = std::move(valid_actions);
284
269
  }
285
270
 
286
271
  int GameStateCpp::get_current_step() const { return current_step_; }
272
+ int GameStateCpp::get_last_cleared_triangles() const { return last_cleared_triangles_; } // Added implementation
287
273
  std::optional<std::string> GameStateCpp::get_game_over_reason() const { return game_over_reason_; }
288
274
 
289
- // Python-facing copy method uses the C++ copy constructor
290
275
  GameStateCpp GameStateCpp::copy() const
291
276
  {
292
277
  return GameStateCpp(*this);
@@ -301,13 +286,14 @@ namespace trianglengin::cpp
301
286
  bool was_occupied = occupied_grid[r][c];
302
287
  occupied_grid[r][c] = !was_occupied;
303
288
  color_grid[r][c] = was_occupied ? NO_COLOR_ID : DEBUG_COLOR_ID;
289
+ last_cleared_triangles_ = 0; // Reset cleared count after manual toggle
304
290
  if (!was_occupied)
305
291
  {
306
292
  // Check for line clears only if a cell becomes occupied
307
- grid_logic::check_and_clear_lines(grid_data_, {{r, c}});
293
+ auto clear_result = grid_logic::check_and_clear_lines(grid_data_, {{r, c}});
294
+ last_cleared_triangles_ = static_cast<int>(std::get<1>(clear_result).size());
308
295
  }
309
- invalidate_action_cache(); // Always invalidate after manual change
310
- // Force recalculation of valid actions and game over state after toggle
296
+ invalidate_action_cache();
311
297
  get_valid_actions(true);
312
298
  }
313
299
  }
@@ -324,18 +310,14 @@ namespace trianglengin::cpp
324
310
  shapes_[i] = std::nullopt;
325
311
  }
326
312
  invalidate_action_cache();
327
- // Force recalculation of valid actions and game over state after setting shapes
328
313
  get_valid_actions(true);
329
314
  }
330
315
 
331
316
  Action GameStateCpp::encode_action(int shape_idx, int r, int c) const
332
317
  {
333
318
  int grid_size = config_.rows * config_.cols;
334
- // Basic bounds check (more robust check in can_place)
335
319
  if (shape_idx < 0 || shape_idx >= config_.num_shape_slots || r < 0 || r >= config_.rows || c < 0 || c >= config_.cols)
336
320
  {
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
321
  throw std::out_of_range("encode_action arguments out of range during valid action calculation.");
340
322
  }
341
323
  return shape_idx * grid_size + r * config_.cols + c;
@@ -1,4 +1,4 @@
1
- // File: src/trianglengin/cpp/game_state.h
1
+
2
2
  #ifndef TRIANGLENGIN_CPP_GAME_STATE_H
3
3
  #define TRIANGLENGIN_CPP_GAME_STATE_H
4
4
 
@@ -40,6 +40,7 @@ namespace trianglengin::cpp
40
40
  double get_score() const;
41
41
  const std::set<Action> &get_valid_actions(bool force_recalculate = false);
42
42
  int get_current_step() const;
43
+ int get_last_cleared_triangles() const; // Added getter
43
44
  std::optional<std::string> get_game_over_reason() const;
44
45
  GameStateCpp copy() const; // Keep Python-facing copy method
45
46
  void debug_toggle_cell(int r, int c);
@@ -62,6 +63,7 @@ namespace trianglengin::cpp
62
63
  std::vector<std::optional<ShapeCpp>> shapes_;
63
64
  double score_;
64
65
  int current_step_;
66
+ int last_cleared_triangles_; // Added member
65
67
  bool game_over_;
66
68
  std::optional<std::string> game_over_reason_;
67
69
  mutable std::optional<std::set<Action>> valid_actions_cache_; // Mutable for const getter
@@ -1,14 +1,13 @@
1
1
  # File: src/trianglengin/game_interface.py
2
2
  import logging
3
3
  import random
4
- from typing import Any, cast # Import necessary types
4
+ from typing import Any, cast
5
5
 
6
6
  import numpy as np
7
7
 
8
8
  from .config import EnvConfig
9
9
 
10
10
  try:
11
- # Keep the alias for clarity within this file
12
11
  import trianglengin.trianglengin_cpp as cpp_module
13
12
  except ImportError as e:
14
13
  raise ImportError(
@@ -27,7 +26,6 @@ class Shape:
27
26
  color: tuple[int, int, int],
28
27
  color_id: int,
29
28
  ):
30
- # Sort triangles for consistent representation and hashing
31
29
  self.triangles: list[tuple[int, int, bool]] = sorted(triangles)
32
30
  self.color: tuple[int, int, int] = color
33
31
  self.color_id: int = color_id
@@ -42,13 +40,11 @@ class Shape:
42
40
 
43
41
  def copy(self) -> "Shape":
44
42
  """Creates a shallow copy."""
45
- # Use sorted triangles in the copy as well
46
43
  return Shape(list(self.triangles), self.color, self.color_id)
47
44
 
48
45
  def __eq__(self, other: object) -> bool:
49
46
  if not isinstance(other, Shape):
50
47
  return NotImplemented
51
- # Comparison relies on sorted triangles
52
48
  return (
53
49
  self.triangles == other.triangles
54
50
  and self.color == other.color
@@ -56,7 +52,6 @@ class Shape:
56
52
  )
57
53
 
58
54
  def __hash__(self) -> int:
59
- # Hash relies on sorted triangles (converted to tuple)
60
55
  return hash((tuple(self.triangles), self.color, self.color_id))
61
56
 
62
57
  def __str__(self) -> str:
@@ -78,7 +73,7 @@ class GameState:
78
73
  Provides a Pythonic interface to the core game logic.
79
74
  """
80
75
 
81
- _cpp_state: cpp_module.GameStateCpp # Add type hint for instance variable
76
+ _cpp_state: cpp_module.GameStateCpp
82
77
 
83
78
  def __init__(
84
79
  self, config: EnvConfig | None = None, initial_seed: int | None = None
@@ -88,7 +83,6 @@ class GameState:
88
83
  initial_seed if initial_seed is not None else random.randint(0, 2**32 - 1)
89
84
  )
90
85
  try:
91
- # Pass the EnvConfig object directly to the C++ constructor binding
92
86
  self._cpp_state = cpp_module.GameStateCpp(self.env_config, used_seed)
93
87
  except Exception as e:
94
88
  log.exception(f"Failed to initialize C++ GameStateCpp: {e}")
@@ -108,24 +102,19 @@ class GameState:
108
102
  Returns: (reward, done)
109
103
  """
110
104
  try:
111
- # The C++ method already returns tuple[double, bool], cast might be redundant
112
- # if pybind handles it, but explicit cast helps mypy if stub is missing details.
113
105
  reward, done = cast("tuple[float, bool]", self._cpp_state.step(action))
114
106
  self._clear_caches()
115
107
  return reward, done
116
108
  except Exception as e:
117
109
  log.exception(f"Error during C++ step execution for action {action}: {e}")
118
- # Return penalty and done=True consistent with C++ logic for errors during step
119
110
  return self.env_config.PENALTY_GAME_OVER, True
120
111
 
121
112
  def is_over(self) -> bool:
122
113
  """Checks if the game is over."""
123
- # C++ returns bool, cast might be redundant but safe for mypy
124
114
  return cast("bool", self._cpp_state.is_over())
125
115
 
126
116
  def game_score(self) -> float:
127
117
  """Returns the current accumulated score."""
128
- # C++ returns double, cast might be redundant but safe for mypy
129
118
  return cast("float", self._cpp_state.get_score())
130
119
 
131
120
  def get_outcome(self) -> float:
@@ -134,18 +123,14 @@ class GameState:
134
123
  Required by MCTS implementations like trimcts.
135
124
  """
136
125
  if self.is_over():
137
- # In this game, the final score is the outcome.
138
- # Adjust if a different outcome definition is needed (e.g., +1 win, -1 loss).
139
126
  return self.game_score()
140
127
  else:
141
- # MCTS typically expects 0 for non-terminal states during simulation.
142
128
  return 0.0
143
129
 
144
130
  def valid_actions(self, force_recalculate: bool = False) -> set[int]:
145
131
  """
146
132
  Returns a set of valid encoded action indices for the current state.
147
133
  """
148
- # C++ returns std::set<int>, pybind converts to Python set. Cast is safe.
149
134
  return cast(
150
135
  "set[int]", set(self._cpp_state.get_valid_actions(force_recalculate))
151
136
  )
@@ -153,14 +138,12 @@ class GameState:
153
138
  def get_shapes(self) -> list[Shape | None]:
154
139
  """Returns the list of current shapes in the preview slots."""
155
140
  if self._cached_shapes is None:
156
- # C++ returns list[Optional[tuple[list[tuple], tuple, int]]]
157
141
  shapes_data = self._cpp_state.get_shapes_cpp()
158
142
  self._cached_shapes = []
159
143
  for data in shapes_data:
160
144
  if data is None:
161
145
  self._cached_shapes.append(None)
162
146
  else:
163
- # Explicitly cast the structure returned by pybind if needed
164
147
  tris_py, color_py, id_py = cast(
165
148
  "tuple[list[tuple[int, int, bool]], tuple[int, int, int], int]",
166
149
  data,
@@ -174,7 +157,6 @@ class GameState:
174
157
  Uses cached data if available.
175
158
  """
176
159
  if self._cached_grid_data is None:
177
- # pybind numpy bindings should return np.ndarray directly
178
160
  occupied_np = self._cpp_state.get_grid_occupied_flat()
179
161
  color_id_np = self._cpp_state.get_grid_colors_flat()
180
162
  death_np = self._cpp_state.get_grid_death_flat()
@@ -190,6 +172,10 @@ class GameState:
190
172
  """Returns the current step count."""
191
173
  return cast("int", self._cpp_state.get_current_step())
192
174
 
175
+ def get_last_cleared_triangles(self) -> int:
176
+ """Returns the number of triangles cleared in the most recent step."""
177
+ return cast("int", self._cpp_state.get_last_cleared_triangles())
178
+
193
179
  def get_game_over_reason(self) -> str | None:
194
180
  """Returns the reason why the game ended, if it's over."""
195
181
  return cast("str | None", self._cpp_state.get_game_over_reason())
@@ -198,7 +184,7 @@ class GameState:
198
184
  """Creates a deep copy of the game state."""
199
185
  new_wrapper = GameState.__new__(GameState)
200
186
  new_wrapper.env_config = self.env_config
201
- new_wrapper._cpp_state = self._cpp_state.copy() # C++ copy handles members
187
+ new_wrapper._cpp_state = self._cpp_state.copy()
202
188
  new_wrapper._cached_shapes = None
203
189
  new_wrapper._cached_grid_data = None
204
190
  return new_wrapper
@@ -212,10 +198,9 @@ class GameState:
212
198
  """
213
199
  Directly sets the shapes in the preview slots. For debugging/testing.
214
200
  """
215
- # Convert Python Shape objects (or None) to the tuple format expected by C++ binding
216
201
  shapes_data = [s.to_cpp_repr() if s else None for s in shapes]
217
202
  self._cpp_state.debug_set_shapes(shapes_data)
218
- self._clear_caches() # Invalidate caches after changing shapes
203
+ self._clear_caches()
219
204
 
220
205
  def _clear_caches(self) -> None:
221
206
  """Clears Python-level caches."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trianglengin
3
- Version: 2.0.6
3
+ Version: 2.0.7
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
@@ -57,17 +57,141 @@ It encapsulates:
57
57
 
58
58
  ---
59
59
 
60
+
61
+
60
62
  ## 🎮 The Ultimate Triangle Puzzle Guide 🧩
61
63
 
62
- *(Game rules remain the same)*
64
+ Get ready to become a Triangle Master! This guide explains everything you need to know to play the game, step-by-step, with lots of details!
65
+
66
+ ### 1. Introduction: Your Mission! 🎯
67
+
68
+ Your goal is to place colorful shapes onto a special triangular grid. By filling up lines of triangles, you make them disappear and score points! Keep placing shapes and clearing lines for as long as possible to get the highest score before the grid fills up and you run out of moves. Sounds simple? Let's dive into the details!
69
+
70
+ ### 2. The Playing Field: The Grid 🗺️
71
+
72
+ - **Triangle Cells:** The game board is a grid made of many small triangles. Some point UP (🔺) and some point DOWN (🔻). They alternate like a checkerboard pattern based on their row and column index (specifically, `(row + col) % 2 != 0` means UP).
73
+ - **Shape:** The grid itself is rectangular overall, but the playable area within it is typically shaped like a triangle or hexagon, wider in the middle and narrower at the top and bottom.
74
+ - **Playable Area:** You can only place shapes within the designated playable area.
75
+ - **Death Zones 💀:** Around the edges of the playable area (often at the start and end of rows), some triangles are marked as "Death Zones". You **cannot** place any part of a shape onto these triangles. They are off-limits! Think of them as the boundaries within the rectangular grid.
76
+
77
+ ### 3. Your Tools: The Shapes 🟦🟥🟩
78
+
79
+ - **Shape Formation:** Each shape is a collection of connected small triangles (🔺 and 🔻). They come in different colors and arrangements. Some might be a single triangle, others might be long lines, L-shapes, or more complex patterns.
80
+ - **Relative Positions:** The triangles within a shape have fixed positions _relative to each other_. When you move the shape, all its triangles move together as one block.
81
+ - **Preview Area:** You will always have **three** shapes available to choose from at any time. These are shown in a special "preview area" on the side of the screen.
82
+
83
+ ### 4. Making Your Move: Placing Shapes 🖱️➡️▦
84
+
85
+ This is the core action! Here's exactly how to place a shape:
86
+
87
+ - **Step 4a: Select a Shape:** Look at the three shapes in the preview area. Click on the one you want to place. It should highlight 💡 to show it's selected.
88
+ - **Step 4b: Aim on the Grid:** Move your mouse cursor over the main grid. You'll see a faint "ghost" image of your selected shape following your mouse. This preview helps you aim.
89
+ - **Step 4c: The Placement Rules (MUST Follow!)**
90
+ - 📏 **Rule 1: Fit Inside Playable Area:** ALL triangles of your chosen shape must land within the playable grid area. No part of the shape can land in a Death Zone 💀.
91
+ - 🧱 **Rule 2: No Overlap:** ALL triangles of your chosen shape must land on currently _empty_ spaces on the grid. You cannot place a shape on top of triangles that are already filled with color from previous shapes.
92
+ - 📐 **Rule 3: Orientation Match!** This is crucial!
93
+ - If a part of your shape is an UP triangle (🔺), it MUST land on an UP space (🔺) on the grid.
94
+ - If a part of your shape is a DOWN triangle (🔻), it MUST land on a DOWN space (🔻) on the grid.
95
+ - 🔺➡️🔺 (OK!)
96
+ - 🔻➡️🔻 (OK!)
97
+ - 🔺➡️🔻 (INVALID! ❌)
98
+ - 🔻➡️🔺 (INVALID! ❌)
99
+ - **Visual Feedback:** The game helps you!
100
+ - 👍 **Valid Spot:** If the position under your mouse follows ALL three rules, the ghost preview will usually look solid and possibly greenish. This means you _can_ place the shape here.
101
+ - 👎 **Invalid Spot:** If the position breaks _any_ of the rules (out of bounds, overlaps, wrong orientation), the ghost preview will usually look faded and possibly reddish. This means you _cannot_ place the shape here.
102
+ - **Step 4d: Confirm Placement:** Once you find a **valid** spot (👍), click the left mouse button again. _Click!_ The shape is now placed permanently on the grid! ✨
103
+
104
+ ### 5. Scoring Points: How You Win! 🏆
105
+
106
+ You score points in two main ways:
107
+
108
+ - **Placing Triangles:** You get a small number of points for _every single small triangle_ that makes up the shape you just placed. (e.g., placing a 3-triangle shape might give you 3 \* tiny_score points).
109
+ - **Clearing Lines:** This is where the BIG points come from! You get a much larger number of points for _every single small triangle_ that disappears when you clear a line (or multiple lines at once!). See the next section for details!
110
+
111
+ ### 6. Line Clearing Magic! ✨ (The Key to High Scores!)
112
+
113
+ This is the most exciting part! When you place a shape, the game immediately checks if you've completed any lines. This section explains how the game _finds_ and _clears_ these lines.
114
+
115
+ - **What Lines Can Be Cleared?** There are **three** types of lines the game looks for:
116
+
117
+ - **Horizontal Lines ↔️:** A straight, unbroken line of filled triangles going across a single row.
118
+ - **Diagonal Lines (Top-Left to Bottom-Right) ↘️:** An unbroken diagonal line of filled triangles stepping down and to the right.
119
+ - **Diagonal Lines (Bottom-Left to Top-Right) ↗️:** An unbroken diagonal line of filled triangles stepping up and to the right.
120
+
121
+ - **How Lines are Found: Pre-calculation of Maximal Lines**
63
122
 
64
- *(... Game rules section remains unchanged ...)*
123
+ - **The Idea:** Instead of checking every possible line combination all the time, the game pre-calculates all *maximal* continuous lines of playable triangles when it starts. A **maximal line** is the longest possible straight segment of *playable* triangles (not in a Death Zone) in one of the three directions (Horizontal, Diagonal ↘️, Diagonal ↗️).
124
+ - **Tracing:** For every playable triangle on the grid, the game traces outwards in each of the three directions to find the full extent of the continuous playable line passing through that triangle in that direction.
125
+ - **Storing Maximal Lines:** Only the complete maximal lines found are stored. For example, if tracing finds a playable sequence `A-B-C-D`, only the line `(A,B,C,D)` is stored, not the sub-segments like `(A,B,C)` or `(B,C,D)`. These maximal lines represent the *potential* lines that can be cleared.
126
+ - **Coordinate Map:** The game also builds a map linking each playable triangle coordinate `(r, c)` to the set of maximal lines it belongs to. This allows for quick lookup.
127
+
128
+ - **Defining the Paths (Neighbor Logic):** How does the game know which triangle is "next" when tracing? It depends on the current triangle's orientation (🔺 or 🔻) and the direction being traced:
129
+
130
+ - **Horizontal ↔️:**
131
+ - Left Neighbor: `(r, c-1)` (Always in the same row)
132
+ - Right Neighbor: `(r, c+1)` (Always in the same row)
133
+ - **Diagonal ↘️ (TL-BR):**
134
+ - If current is 🔺 (Up): Next is `(r+1, c)` (Down triangle directly below)
135
+ - If current is 🔻 (Down): Next is `(r, c+1)` (Up triangle to the right)
136
+ - **Diagonal ↗️ (BL-TR):**
137
+ - If current is 🔻 (Down): Next is `(r-1, c)` (Up triangle directly above)
138
+ - If current is 🔺 (Up): Next is `(r, c+1)` (Down triangle to the right)
139
+
140
+ - **Visualizing the Paths:**
141
+
142
+ - **Horizontal ↔️:**
143
+ ```
144
+ ... [🔻][🔺][🔻][🔺][🔻][🔺] ... (Moves left/right in the same row)
145
+ ```
146
+ - **Diagonal ↘️ (TL-BR):** (Connects via shared horizontal edges)
147
+ ```
148
+ ...[🔺]...
149
+ ...[🔻][🔺] ...
150
+ ... [🔻][🔺] ...
151
+ ... [🔻] ...
152
+ (Path alternates row/col increments depending on orientation)
153
+ ```
154
+ - **Diagonal ↗️ (BL-TR):** (Connects via shared horizontal edges)
155
+ ```
156
+ ... [🔺] ...
157
+ ... [🔺][🔻] ...
158
+ ... [🔺][🔻] ...
159
+ ... [🔻] ...
160
+ (Path alternates row/col increments depending on orientation)
161
+ ```
162
+
163
+ - **The "Full Line" Rule:** After you place a piece, the game looks at the coordinates `(r, c)` of the triangles you just placed. Using the pre-calculated map, it finds all the *maximal* lines that contain _any_ of those coordinates. For each of those maximal lines (that have at least 2 triangles), it checks: "Is _every single triangle coordinate_ in this maximal line now occupied?" If yes, that line is complete! (Note: Single isolated triangles don't count as clearable lines).
164
+
165
+ - **The _Poof_! 💨:**
166
+ - If placing your shape completes one or MORE maximal lines (of any type, length >= 2) simultaneously, all the triangles in ALL completed lines vanish instantly!
167
+ - The spaces become empty again.
168
+ - You score points for _every single triangle_ that vanished. Clearing multiple lines at once is the best way to rack up points! 🥳
169
+
170
+ ### 7. Getting New Shapes: The Refill 🪄
171
+
172
+ - **The Trigger:** The game only gives you new shapes when a specific condition is met.
173
+ - **The Condition:** New shapes appear **only when all three of your preview slots become empty at the exact same time.**
174
+ - **How it Happens:** This usually occurs right after you place your _last_ available shape (the third one).
175
+ - **The Refill:** As soon as the third slot becomes empty, _BAM!_ 🪄 Three brand new, randomly generated shapes instantly appear in the preview slots.
176
+ - **Important:** If you place a shape and only one or two slots are empty, you **do not** get new shapes yet. You must use up all three before the refill happens.
177
+
178
+ ### 8. The End of the Road: Game Over 😭
179
+
180
+ So, how does the game end?
181
+
182
+ - **The Condition:** The game is over when you **cannot legally place _any_ of the three shapes currently available in your preview slots anywhere on the grid.**
183
+ - **The Check:** After every move (placing a shape and any resulting line clears), and after any potential shape refill, the game checks: "Is there at least one valid spot on the grid for Shape 1? OR for Shape 2? OR for Shape 3?"
184
+ - **No More Moves:** If the answer is "NO" for all three shapes (meaning none of them can be placed anywhere according to the Placement Rules), then the game immediately ends.
185
+ - **Strategy:** This means you need to be careful! Don't fill up the grid in a way that leaves no room for the types of shapes you might get later. Always try to keep options open! 🤔
186
+
187
+ That's it! Now you know all the rules. Go forth and conquer the Triangle Puzzle! 🏆
65
188
 
66
189
  ---
67
190
 
68
191
  ## Purpose
69
192
 
70
- The primary goal is to provide a self-contained, installable library with a high-performance C++ core and a Python interface for the triangle puzzle game. This allows different RL agent implementations or other applications to build upon a consistent and fast game backend. The interactive UI is included but only initialized when running the specific UI commands.
193
+ The primary goal is to provide a self-contained, installable library for the core logic and basic interactive UI of the triangle puzzle game. This allows different RL agent implementations or other applications to build upon a consistent and well-defined game backend, avoiding code duplication.
194
+
71
195
 
72
196
  ## Installation
73
197
 
@@ -1,18 +1,18 @@
1
1
  trianglengin/__init__.py,sha256=VBYKMivxX19prPkxmSCSJJrjFXDVWkQJ9b5kQ6RzvkI,954
2
- trianglengin/game_interface.py,sha256=7zvt61QGmAuagRfNMpRORhX11sy9NqWM9xWstsEawKY,9540
2
+ trianglengin/game_interface.py,sha256=5NkM-mj--iBCFQzpsR6WQsBZnYRrvEqQl0OQ6UPA0Jg,8184
3
3
  trianglengin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- trianglengin/trianglengin_cpp.cp310-win32.pyd,sha256=BcPedX7TCP7AgjzMKeZdX9NOA0fkM42iTqWfnAZ4rfs,241664
5
- trianglengin/Release/trianglengin_cpp.cp39-win_amd64.pyd,sha256=BcPedX7TCP7AgjzMKeZdX9NOA0fkM42iTqWfnAZ4rfs,241664
4
+ trianglengin/trianglengin_cpp.cp310-win32.pyd,sha256=t3DK8DqPQoX-HB4rIa4qoX0T5UxLwbnTa68YwshOTgU,242176
5
+ trianglengin/Release/trianglengin_cpp.cp39-win_amd64.pyd,sha256=t3DK8DqPQoX-HB4rIa4qoX0T5UxLwbnTa68YwshOTgU,242176
6
6
  trianglengin/config/README.md,sha256=zPN8ezwsN5fmH4xCsVSgOZwfyng_TwrCj3MlwEq63Y4,1544
7
7
  trianglengin/config/__init__.py,sha256=sdr4D-pSCYmfXY2jOLEjPra2VewDiaWNwx1bWKp0nf8,168
8
8
  trianglengin/config/display_config.py,sha256=ivZEGTCuUfmiVU6GJpVpEASFh30AAHSH42PIIi6bPZg,1862
9
9
  trianglengin/config/env_config.py,sha256=hJmdAm4HC5SWpAtp7u3tIDWPMKw711rVszwyQWD6-Ys,2421
10
10
  trianglengin/core/__init__.py,sha256=LWZkoJctRGUhd5DZUJlPCS7usdQLZyKYkoyWUWO3L4o,470
11
11
  trianglengin/cpp/CMakeLists.txt,sha256=vmhTQLRD7LRPbfFlisVVJ9-cCkeM3UMrikIde7TRLWc,1370
12
- trianglengin/cpp/bindings.cpp,sha256=okdR2NCdxgP_FTgMjwXNbzcaFdERRrvy7m1eB_5FY8g,8899
12
+ trianglengin/cpp/bindings.cpp,sha256=gmJdAVIHsusPC6Hz_2Ya_ItpOZ2Ajl9ZMMJMCKkwVn4,8652
13
13
  trianglengin/cpp/config.h,sha256=rZZ6os70igGgA_fp-81fMv9TQMFJb91m6T-KeTOpKAU,742
14
- trianglengin/cpp/game_state.cpp,sha256=TTwJm-9BBvof2tntAVdQ5fjMSjqAh2F4wFsMkFlgn48,13068
15
- trianglengin/cpp/game_state.h,sha256=no5D0SP-vijTuL-sObJ4tx06nW0oaX42mzMZn1matL8,3302
14
+ trianglengin/cpp/game_state.cpp,sha256=U8po3NVAuJkq_az1bA7raxkMWK8HHM_zcrEcttb74F8,10771
15
+ trianglengin/cpp/game_state.h,sha256=reZVI6C5Sj-zn7XaVNhKNfJkaIqmlPgAOpIp0hb-Iv8,3371
16
16
  trianglengin/cpp/grid_data.cpp,sha256=6iucpVisR1dYayKjSPbjCaYEKaqWaNqGVswyLm6f-zM,8024
17
17
  trianglengin/cpp/grid_data.h,sha256=HVH5gqXAuHwDJrC2d7uy_maeS2ulckeGy68TkpL9gNg,2568
18
18
  trianglengin/cpp/grid_logic.cpp,sha256=O8CfzMw_3r1fU0Iu3KnapPBi5kC7tSs47yWeaVsIxOw,3457
@@ -51,9 +51,9 @@ trianglengin/ui/visualization/drawing/utils.py,sha256=gtGtt8bgaIvctFA5TEmKqmXNov
51
51
  trianglengin/utils/__init__.py,sha256=pnFOkerF1TAz9oCzkNYBw6N56uENsyOiMZzA0cQ2SrA,185
52
52
  trianglengin/utils/geometry.py,sha256=nrhr5C3MGO-_xXz96w1B0OJNaATLbc_AeZwoB_O6gsI,2128
53
53
  trianglengin/utils/types.py,sha256=N_CjVJISvBP5QG1QYvepgORD0oNpMJlJRCJ9IdgjmWc,241
54
- trianglengin-2.0.6.dist-info/licenses/LICENSE,sha256=DNjSCXzwafRxA5zjyWccoRXZTDMpTNElFpNVKz4NEpY,1098
55
- trianglengin-2.0.6.dist-info/METADATA,sha256=p_tcQFMpt0tSbMQ5R9lgaGXnfMvnvjMFSdjyJ1G_TNU,12316
56
- trianglengin-2.0.6.dist-info/WHEEL,sha256=CNoUkshc3SqngBVkyzktNNHC8941NRkLClTTB1cTnI8,97
57
- trianglengin-2.0.6.dist-info/entry_points.txt,sha256=kQEqO_U-MEpMEC0xwOPSucBzQIq2Ny7XwCtFSruZhvY,57
58
- trianglengin-2.0.6.dist-info/top_level.txt,sha256=YsSWmp_2zM23wRc5TRERHpVCgQuVYieYHDTpnwVQC7Y,13
59
- trianglengin-2.0.6.dist-info/RECORD,,
54
+ trianglengin-2.0.7.dist-info/licenses/LICENSE,sha256=DNjSCXzwafRxA5zjyWccoRXZTDMpTNElFpNVKz4NEpY,1098
55
+ trianglengin-2.0.7.dist-info/METADATA,sha256=4gqlUZG-ggZL28DVboY7EaMwUTVeBKjbb9D6tuZs2hw,22441
56
+ trianglengin-2.0.7.dist-info/WHEEL,sha256=CNoUkshc3SqngBVkyzktNNHC8941NRkLClTTB1cTnI8,97
57
+ trianglengin-2.0.7.dist-info/entry_points.txt,sha256=kQEqO_U-MEpMEC0xwOPSucBzQIq2Ny7XwCtFSruZhvY,57
58
+ trianglengin-2.0.7.dist-info/top_level.txt,sha256=YsSWmp_2zM23wRc5TRERHpVCgQuVYieYHDTpnwVQC7Y,13
59
+ trianglengin-2.0.7.dist-info/RECORD,,