trianglengin 2.0.4__pp310-pypy310_pp73-macosx_11_0_arm64.whl → 2.0.7__pp310-pypy310_pp73-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.
- trianglengin/cpp/bindings.cpp +5 -8
- trianglengin/cpp/game_state.cpp +69 -81
- trianglengin/cpp/game_state.h +13 -2
- trianglengin/cpp/grid_data.cpp +18 -5
- trianglengin/cpp/grid_data.h +4 -5
- trianglengin/cpp/structs.h +30 -1
- trianglengin/game_interface.py +8 -23
- trianglengin/trianglengin_cpp.pypy310-pp73-darwin.so +0 -0
- {trianglengin-2.0.4.dist-info → trianglengin-2.0.7.dist-info}/METADATA +128 -4
- {trianglengin-2.0.4.dist-info → trianglengin-2.0.7.dist-info}/RECORD +14 -14
- {trianglengin-2.0.4.dist-info → trianglengin-2.0.7.dist-info}/WHEEL +0 -0
- {trianglengin-2.0.4.dist-info → trianglengin-2.0.7.dist-info}/entry_points.txt +0 -0
- {trianglengin-2.0.4.dist-info → trianglengin-2.0.7.dist-info}/licenses/LICENSE +0 -0
- {trianglengin-2.0.4.dist-info → trianglengin-2.0.7.dist-info}/top_level.txt +0 -0
trianglengin/cpp/bindings.cpp
CHANGED
@@ -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>
|
7
|
+
#include <pybind11/functional.h>
|
7
8
|
#include <vector>
|
8
9
|
#include <stdexcept>
|
9
|
-
#include <cstring>
|
10
|
-
#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
|
}
|
trianglengin/cpp/game_state.cpp
CHANGED
@@ -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>
|
8
|
+
#include <iostream>
|
8
9
|
#include <algorithm> // For std::min
|
9
10
|
|
10
11
|
namespace trianglengin::cpp
|
@@ -16,18 +17,54 @@ namespace trianglengin::cpp
|
|
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
|
{
|
22
24
|
reset();
|
23
25
|
}
|
24
26
|
|
27
|
+
// --- Explicit Copy Constructor ---
|
28
|
+
GameStateCpp::GameStateCpp(const GameStateCpp &other)
|
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_)
|
39
|
+
{
|
40
|
+
}
|
41
|
+
|
42
|
+
// --- Explicit Copy Assignment Operator ---
|
43
|
+
GameStateCpp &GameStateCpp::operator=(const GameStateCpp &other)
|
44
|
+
{
|
45
|
+
if (this != &other)
|
46
|
+
{
|
47
|
+
config_ = other.config_;
|
48
|
+
grid_data_ = other.grid_data_;
|
49
|
+
shapes_ = other.shapes_;
|
50
|
+
score_ = other.score_;
|
51
|
+
current_step_ = other.current_step_;
|
52
|
+
last_cleared_triangles_ = other.last_cleared_triangles_; // Copy added member
|
53
|
+
game_over_ = other.game_over_;
|
54
|
+
game_over_reason_ = other.game_over_reason_;
|
55
|
+
valid_actions_cache_ = other.valid_actions_cache_;
|
56
|
+
rng_ = other.rng_;
|
57
|
+
}
|
58
|
+
return *this;
|
59
|
+
}
|
60
|
+
|
25
61
|
void GameStateCpp::reset()
|
26
62
|
{
|
27
63
|
grid_data_.reset();
|
28
64
|
std::fill(shapes_.begin(), shapes_.end(), std::nullopt);
|
29
65
|
score_ = 0.0;
|
30
66
|
current_step_ = 0;
|
67
|
+
last_cleared_triangles_ = 0; // Reset added member
|
31
68
|
game_over_ = false;
|
32
69
|
game_over_reason_ = std::nullopt;
|
33
70
|
valid_actions_cache_ = std::nullopt;
|
@@ -37,29 +74,25 @@ namespace trianglengin::cpp
|
|
37
74
|
|
38
75
|
void GameStateCpp::check_initial_state_game_over()
|
39
76
|
{
|
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
|
-
// REMOVED C++ Log: std::cerr << "[GameStateCpp::check_initial_state_game_over] Forced game over: No valid actions at start." << std::endl;
|
45
|
-
}
|
77
|
+
get_valid_actions(true);
|
46
78
|
}
|
47
79
|
|
48
80
|
std::tuple<double, bool> GameStateCpp::step(Action action)
|
49
81
|
{
|
82
|
+
last_cleared_triangles_ = 0; // Reset before potential clearing
|
83
|
+
|
50
84
|
if (game_over_)
|
51
85
|
{
|
52
86
|
return {0.0, true};
|
53
87
|
}
|
54
88
|
|
55
89
|
const auto &valid_actions = get_valid_actions();
|
90
|
+
|
56
91
|
if (valid_actions.find(action) == valid_actions.end())
|
57
92
|
{
|
58
|
-
// Invalid action detected
|
59
93
|
force_game_over("Invalid action provided: " + std::to_string(action));
|
60
|
-
// Apply penalty to score *before* returning
|
61
94
|
score_ += config_.penalty_game_over;
|
62
|
-
return {config_.penalty_game_over, true};
|
95
|
+
return {config_.penalty_game_over, true};
|
63
96
|
}
|
64
97
|
|
65
98
|
int shape_idx, r, c;
|
@@ -69,17 +102,15 @@ namespace trianglengin::cpp
|
|
69
102
|
}
|
70
103
|
catch (const std::out_of_range &e)
|
71
104
|
{
|
72
|
-
// Error during decoding (should be caught by valid_actions check, but defensive)
|
73
105
|
force_game_over("Failed to decode action: " + std::to_string(action));
|
74
|
-
score_ += config_.penalty_game_over;
|
106
|
+
score_ += config_.penalty_game_over;
|
75
107
|
return {config_.penalty_game_over, true};
|
76
108
|
}
|
77
109
|
|
78
110
|
if (shape_idx < 0 || shape_idx >= static_cast<int>(shapes_.size()) || !shapes_[shape_idx].has_value())
|
79
111
|
{
|
80
|
-
// Action references invalid slot (should be caught by valid_actions check)
|
81
112
|
force_game_over("Action references invalid/empty shape slot: " + std::to_string(shape_idx));
|
82
|
-
score_ += config_.penalty_game_over;
|
113
|
+
score_ += config_.penalty_game_over;
|
83
114
|
return {config_.penalty_game_over, true};
|
84
115
|
}
|
85
116
|
|
@@ -87,9 +118,8 @@ namespace trianglengin::cpp
|
|
87
118
|
|
88
119
|
if (!grid_logic::can_place(grid_data_, shape_to_place, r, c))
|
89
120
|
{
|
90
|
-
// Should not happen if valid_actions is correct
|
91
121
|
force_game_over("Placement check failed for valid action (logic error?). Action: " + std::to_string(action));
|
92
|
-
score_ += config_.penalty_game_over;
|
122
|
+
score_ += config_.penalty_game_over;
|
93
123
|
return {config_.penalty_game_over, true};
|
94
124
|
}
|
95
125
|
|
@@ -108,7 +138,7 @@ namespace trianglengin::cpp
|
|
108
138
|
if (!grid_data_.is_valid(target_r, target_c) || grid_data_.is_death(target_r, target_c))
|
109
139
|
{
|
110
140
|
force_game_over("Attempted placement out of bounds/death zone during execution. Action: " + std::to_string(action));
|
111
|
-
score_ += config_.penalty_game_over;
|
141
|
+
score_ += config_.penalty_game_over;
|
112
142
|
return {config_.penalty_game_over, true};
|
113
143
|
}
|
114
144
|
occupied_grid[target_r][target_c] = true;
|
@@ -125,6 +155,7 @@ namespace trianglengin::cpp
|
|
125
155
|
std::tie(lines_cleared_count, cleared_coords, cleared_lines_fs) =
|
126
156
|
grid_logic::check_and_clear_lines(grid_data_, newly_occupied_coords);
|
127
157
|
int cleared_count = static_cast<int>(cleared_coords.size());
|
158
|
+
last_cleared_triangles_ = cleared_count; // Store cleared count
|
128
159
|
|
129
160
|
// --- Refill ---
|
130
161
|
bool all_slots_empty = true;
|
@@ -143,9 +174,8 @@ namespace trianglengin::cpp
|
|
143
174
|
|
144
175
|
// --- Update State & Check Game Over ---
|
145
176
|
current_step_++;
|
146
|
-
|
147
|
-
get_valid_actions(true);
|
148
|
-
// game_over_ flag is now definitive
|
177
|
+
invalidate_action_cache();
|
178
|
+
get_valid_actions(true);
|
149
179
|
|
150
180
|
// --- Calculate Reward & Update Score ---
|
151
181
|
double reward = 0.0;
|
@@ -154,37 +184,24 @@ namespace trianglengin::cpp
|
|
154
184
|
|
155
185
|
if (game_over_)
|
156
186
|
{
|
157
|
-
|
187
|
+
// Penalty already applied if game over was due to invalid action this step.
|
158
188
|
}
|
159
189
|
else
|
160
190
|
{
|
161
191
|
reward += config_.reward_per_step_alive;
|
162
192
|
}
|
163
|
-
score_ += reward;
|
193
|
+
score_ += reward;
|
164
194
|
|
165
195
|
return {reward, game_over_};
|
166
196
|
}
|
167
197
|
|
168
198
|
bool GameStateCpp::is_over() const
|
169
199
|
{
|
170
|
-
|
171
|
-
return true;
|
172
|
-
// If cache exists, use it. Otherwise, calculate.
|
173
|
-
if (valid_actions_cache_.has_value())
|
174
|
-
return valid_actions_cache_->empty();
|
175
|
-
// Need to remove const to calculate and potentially set game_over_
|
176
|
-
const_cast<GameStateCpp *>(this)->calculate_valid_actions_internal();
|
177
|
-
// Check game_over_ again as calculate might have set it
|
178
|
-
if (game_over_)
|
179
|
-
return true;
|
180
|
-
// Now the cache should exist
|
181
|
-
return valid_actions_cache_->empty();
|
200
|
+
return game_over_;
|
182
201
|
}
|
183
202
|
|
184
203
|
void GameStateCpp::force_game_over(const std::string &reason)
|
185
204
|
{
|
186
|
-
// This function only sets the flags and reason.
|
187
|
-
// Score update should happen in the context where the game over is triggered (e.g., step).
|
188
205
|
if (!game_over_)
|
189
206
|
{
|
190
207
|
game_over_ = true;
|
@@ -208,17 +225,19 @@ namespace trianglengin::cpp
|
|
208
225
|
}
|
209
226
|
return *valid_actions_cache_;
|
210
227
|
}
|
228
|
+
|
211
229
|
if (!force_recalculate && valid_actions_cache_.has_value())
|
212
230
|
{
|
213
231
|
return *valid_actions_cache_;
|
214
232
|
}
|
233
|
+
|
215
234
|
calculate_valid_actions_internal();
|
216
|
-
|
235
|
+
|
217
236
|
if (!game_over_ && valid_actions_cache_->empty())
|
218
237
|
{
|
219
238
|
force_game_over("No valid actions available.");
|
220
|
-
// REMOVED C++ Log: std::cerr << "[GameStateCpp::get_valid_actions] Forced game over: No valid actions found after calculation." << std::endl;
|
221
239
|
}
|
240
|
+
|
222
241
|
return *valid_actions_cache_;
|
223
242
|
}
|
224
243
|
|
@@ -227,19 +246,9 @@ namespace trianglengin::cpp
|
|
227
246
|
valid_actions_cache_ = std::nullopt;
|
228
247
|
}
|
229
248
|
|
230
|
-
|
231
|
-
void GameStateCpp::calculate_valid_actions_internal() const // Keep const for now, use mutable cache
|
249
|
+
void GameStateCpp::calculate_valid_actions_internal() const
|
232
250
|
{
|
233
|
-
if (game_over_)
|
234
|
-
{
|
235
|
-
valid_actions_cache_ = std::set<Action>();
|
236
|
-
// REMOVED C++ Log: std::cerr << "[GameStateCpp::calculate_valid_actions_internal] Game already over. Returning empty set." << std::endl;
|
237
|
-
return;
|
238
|
-
}
|
239
251
|
std::set<Action> valid_actions;
|
240
|
-
int can_place_true_count = 0; // Counter for debugging
|
241
|
-
int attempts_count = 0; // Counter for total placement checks
|
242
|
-
|
243
252
|
for (int shape_idx = 0; shape_idx < static_cast<int>(shapes_.size()); ++shape_idx)
|
244
253
|
{
|
245
254
|
if (!shapes_[shape_idx].has_value())
|
@@ -247,46 +256,25 @@ namespace trianglengin::cpp
|
|
247
256
|
const ShapeCpp &shape = shapes_[shape_idx].value();
|
248
257
|
for (int r = 0; r < config_.rows; ++r)
|
249
258
|
{
|
250
|
-
|
251
|
-
for (int c = start_c; c < end_c; ++c)
|
259
|
+
for (int c = 0; c < config_.cols; ++c)
|
252
260
|
{
|
253
|
-
|
254
|
-
bool can_place_result = grid_logic::can_place(grid_data_, shape, r, c); // Store result
|
255
|
-
if (can_place_result)
|
261
|
+
if (grid_logic::can_place(grid_data_, shape, r, c))
|
256
262
|
{
|
257
263
|
valid_actions.insert(encode_action(shape_idx, r, c));
|
258
|
-
can_place_true_count++; // Increment counter
|
259
264
|
}
|
260
265
|
}
|
261
266
|
}
|
262
267
|
}
|
263
|
-
// REMOVED C++ Log:
|
264
|
-
// std::cerr << "[GameStateCpp::calculate_valid_actions_internal] Step: " << current_step_
|
265
|
-
// << ", Attempts: " << attempts_count
|
266
|
-
// << ", CanPlaceTrue: " << can_place_true_count
|
267
|
-
// << ", ValidActionsFound: " << valid_actions.size()
|
268
|
-
// << std::endl;
|
269
|
-
|
270
|
-
// Use mutable cache
|
271
268
|
valid_actions_cache_ = std::move(valid_actions);
|
272
269
|
}
|
273
270
|
|
274
271
|
int GameStateCpp::get_current_step() const { return current_step_; }
|
272
|
+
int GameStateCpp::get_last_cleared_triangles() const { return last_cleared_triangles_; } // Added implementation
|
275
273
|
std::optional<std::string> GameStateCpp::get_game_over_reason() const { return game_over_reason_; }
|
276
274
|
|
277
275
|
GameStateCpp GameStateCpp::copy() const
|
278
276
|
{
|
279
|
-
GameStateCpp
|
280
|
-
// Copy the cache state explicitly
|
281
|
-
if (this->valid_actions_cache_.has_value())
|
282
|
-
{
|
283
|
-
newState.valid_actions_cache_ = this->valid_actions_cache_;
|
284
|
-
}
|
285
|
-
else
|
286
|
-
{
|
287
|
-
newState.valid_actions_cache_ = std::nullopt;
|
288
|
-
}
|
289
|
-
return newState;
|
277
|
+
return GameStateCpp(*this);
|
290
278
|
}
|
291
279
|
|
292
280
|
void GameStateCpp::debug_toggle_cell(int r, int c)
|
@@ -298,31 +286,31 @@ namespace trianglengin::cpp
|
|
298
286
|
bool was_occupied = occupied_grid[r][c];
|
299
287
|
occupied_grid[r][c] = !was_occupied;
|
300
288
|
color_grid[r][c] = was_occupied ? NO_COLOR_ID : DEBUG_COLOR_ID;
|
289
|
+
last_cleared_triangles_ = 0; // Reset cleared count after manual toggle
|
301
290
|
if (!was_occupied)
|
302
291
|
{
|
303
292
|
// Check for line clears only if a cell becomes occupied
|
304
|
-
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());
|
305
295
|
}
|
306
|
-
invalidate_action_cache();
|
296
|
+
invalidate_action_cache();
|
297
|
+
get_valid_actions(true);
|
307
298
|
}
|
308
299
|
}
|
309
300
|
|
310
301
|
void GameStateCpp::debug_set_shapes(const std::vector<std::optional<ShapeCpp>> &new_shapes)
|
311
302
|
{
|
312
|
-
// Overwrite the current shapes with the provided ones.
|
313
|
-
// Ensure the size matches the number of slots.
|
314
303
|
size_t num_to_copy = std::min(new_shapes.size(), shapes_.size());
|
315
304
|
for (size_t i = 0; i < num_to_copy; ++i)
|
316
305
|
{
|
317
306
|
shapes_[i] = new_shapes[i];
|
318
307
|
}
|
319
|
-
// Fill remaining slots with nullopt if new_shapes is shorter
|
320
308
|
for (size_t i = num_to_copy; i < shapes_.size(); ++i)
|
321
309
|
{
|
322
310
|
shapes_[i] = std::nullopt;
|
323
311
|
}
|
324
|
-
// Invalidate cache as valid actions will change
|
325
312
|
invalidate_action_cache();
|
313
|
+
get_valid_actions(true);
|
326
314
|
}
|
327
315
|
|
328
316
|
Action GameStateCpp::encode_action(int shape_idx, int r, int c) const
|
@@ -330,7 +318,7 @@ namespace trianglengin::cpp
|
|
330
318
|
int grid_size = config_.rows * config_.cols;
|
331
319
|
if (shape_idx < 0 || shape_idx >= config_.num_shape_slots || r < 0 || r >= config_.rows || c < 0 || c >= config_.cols)
|
332
320
|
{
|
333
|
-
throw std::out_of_range("encode_action arguments out of range.");
|
321
|
+
throw std::out_of_range("encode_action arguments out of range during valid action calculation.");
|
334
322
|
}
|
335
323
|
return shape_idx * grid_size + r * config_.cols + c;
|
336
324
|
}
|
trianglengin/cpp/game_state.h
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
|
2
2
|
#ifndef TRIANGLENGIN_CPP_GAME_STATE_H
|
3
3
|
#define TRIANGLENGIN_CPP_GAME_STATE_H
|
4
4
|
|
@@ -27,14 +27,22 @@ 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;
|
33
40
|
double get_score() const;
|
34
41
|
const std::set<Action> &get_valid_actions(bool force_recalculate = false);
|
35
42
|
int get_current_step() const;
|
43
|
+
int get_last_cleared_triangles() const; // Added getter
|
36
44
|
std::optional<std::string> get_game_over_reason() const;
|
37
|
-
GameStateCpp copy() const;
|
45
|
+
GameStateCpp copy() const; // Keep Python-facing copy method
|
38
46
|
void debug_toggle_cell(int r, int c);
|
39
47
|
void invalidate_action_cache(); // Moved to public
|
40
48
|
// Debug method to force shapes into slots
|
@@ -46,6 +54,8 @@ namespace trianglengin::cpp
|
|
46
54
|
const std::vector<std::optional<ShapeCpp>> &get_shapes() const { return shapes_; }
|
47
55
|
std::vector<std::optional<ShapeCpp>> &get_shapes_mut() { return shapes_; }
|
48
56
|
const EnvConfigCpp &get_config() const { return config_; }
|
57
|
+
// Expose RNG state for copying if needed (or handle seeding in copy)
|
58
|
+
std::mt19937 get_rng_state() const { return rng_; }
|
49
59
|
|
50
60
|
private:
|
51
61
|
EnvConfigCpp config_;
|
@@ -53,6 +63,7 @@ namespace trianglengin::cpp
|
|
53
63
|
std::vector<std::optional<ShapeCpp>> shapes_;
|
54
64
|
double score_;
|
55
65
|
int current_step_;
|
66
|
+
int last_cleared_triangles_; // Added member
|
56
67
|
bool game_over_;
|
57
68
|
std::optional<std::string> game_over_reason_;
|
58
69
|
mutable std::optional<std::set<Action>> valid_actions_cache_; // Mutable for const getter
|
trianglengin/cpp/grid_data.cpp
CHANGED
@@ -44,7 +44,7 @@ namespace trianglengin::cpp
|
|
44
44
|
precompute_lines();
|
45
45
|
}
|
46
46
|
|
47
|
-
// Copy constructor:
|
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:
|
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
|
-
|
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
|
-
|
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
|
{
|
trianglengin/cpp/grid_data.h
CHANGED
@@ -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
|
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
|
trianglengin/cpp/structs.h
CHANGED
@@ -8,7 +8,9 @@
|
|
8
8
|
#include <tuple>
|
9
9
|
#include <string>
|
10
10
|
#include <cstdint>
|
11
|
-
#include <utility>
|
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
|
trianglengin/game_interface.py
CHANGED
@@ -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
|
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
|
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()
|
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()
|
203
|
+
self._clear_caches()
|
219
204
|
|
220
205
|
def _clear_caches(self) -> None:
|
221
206
|
"""Clears Python-level caches."""
|
Binary file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: trianglengin
|
3
|
-
Version: 2.0.
|
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
|
-
|
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
|
-
*(
|
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
|
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,13 +1,7 @@
|
|
1
|
-
trianglengin-
|
2
|
-
trianglengin-2.0.4.dist-info/WHEEL,sha256=8UigNX42SJ7Bc2aBUBR_PNKLdWLJBQB8todoqHTAdoY,116
|
3
|
-
trianglengin-2.0.4.dist-info/entry_points.txt,sha256=kQEqO_U-MEpMEC0xwOPSucBzQIq2Ny7XwCtFSruZhvY,57
|
4
|
-
trianglengin-2.0.4.dist-info/top_level.txt,sha256=YsSWmp_2zM23wRc5TRERHpVCgQuVYieYHDTpnwVQC7Y,13
|
5
|
-
trianglengin-2.0.4.dist-info/METADATA,sha256=Ep7c5ldlwGwB_JhzXsgAWju0AHq4k5144Z5dzqmmR6Q,12066
|
6
|
-
trianglengin-2.0.4.dist-info/licenses/LICENSE,sha256=So3rgoJp-HgoxkclxZLIBC3pmmTwshN4tUO8KiQ6akc,1077
|
7
|
-
trianglengin/trianglengin_cpp.pypy310-pp73-darwin.so,sha256=OeWk67c26OdRho8r9-AuGnswV4K6Yn-dBZfruqmb1GQ,233528
|
1
|
+
trianglengin/trianglengin_cpp.pypy310-pp73-darwin.so,sha256=lC_53hyhxt9RfudMdLQgR2lpoRTchQiFniPp3pB9wwI,233528
|
8
2
|
trianglengin/__init__.py,sha256=4INnGvOYIjzOyCew7lhX6Irc8_H7QVxWc5jopt9TCZ4,919
|
9
3
|
trianglengin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
trianglengin/game_interface.py,sha256=
|
4
|
+
trianglengin/game_interface.py,sha256=2zwSGMWg8GJtAXykOKTAwjAhihl9FAcKoz07-zeYNuY,7963
|
11
5
|
trianglengin/ui/config.py,sha256=E68Qvbr3dwyhC_Rh_PK-QHWlHOrXFQhAsARZZMoQHNs,1579
|
12
6
|
trianglengin/ui/__init__.py,sha256=AVIFYOWoZTt69NnkX0Ht3dEAPMpxQ6WZ8G9tDvxNXvA,558
|
13
7
|
trianglengin/ui/README.md,sha256=M-5UU-NKG6qYu1riCUkftRZIwE2Jvf-5qOZ1QGWjPSg,1718
|
@@ -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=
|
52
|
-
trianglengin/cpp/grid_data.h,sha256=
|
53
|
-
trianglengin/cpp/game_state.h,sha256=
|
54
|
-
trianglengin/cpp/game_state.cpp,sha256=
|
55
|
-
trianglengin/cpp/structs.h,sha256=
|
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=DLAWM3tVMpKMBo1LJjvWCwqeHYAh9aiT27b74dOhnnI,3288
|
48
|
+
trianglengin/cpp/game_state.cpp,sha256=qrXPYYnQnlFjbgqY5e85GEldUkjCdusSr-JsFlouGf4,10431
|
49
|
+
trianglengin/cpp/structs.h,sha256=yGJp_VatwCHpDAcYJBpsIfEMEG4BQuqdDObqd7nBY7M,2155
|
56
50
|
trianglengin/cpp/shape_logic.h,sha256=QvjLAPG5y42FetpXXuCcYrWlcwvKGo7csU8Ch-RmdFs,626
|
57
|
-
trianglengin/cpp/bindings.cpp,sha256=
|
51
|
+
trianglengin/cpp/bindings.cpp,sha256=gCtvBxKrLRLO0OLXt-nbRn3opVMXftkEQrTkpE9G0Zc,8445
|
58
52
|
trianglengin/cpp/shape_logic.cpp,sha256=gF1mgolaNvxpNphwDScNUvOkoq4FuxFASB3n50WI3Bo,4133
|
53
|
+
trianglengin-2.0.7.dist-info/RECORD,,
|
54
|
+
trianglengin-2.0.7.dist-info/WHEEL,sha256=8UigNX42SJ7Bc2aBUBR_PNKLdWLJBQB8todoqHTAdoY,116
|
55
|
+
trianglengin-2.0.7.dist-info/entry_points.txt,sha256=kQEqO_U-MEpMEC0xwOPSucBzQIq2Ny7XwCtFSruZhvY,57
|
56
|
+
trianglengin-2.0.7.dist-info/top_level.txt,sha256=YsSWmp_2zM23wRc5TRERHpVCgQuVYieYHDTpnwVQC7Y,13
|
57
|
+
trianglengin-2.0.7.dist-info/METADATA,sha256=utkuPTxtDnky8-jg8KAa1Ec_bwbAiWnWNerFjjh6BhU,22067
|
58
|
+
trianglengin-2.0.7.dist-info/licenses/LICENSE,sha256=So3rgoJp-HgoxkclxZLIBC3pmmTwshN4tUO8KiQ6akc,1077
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|