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.
- trianglengin/Release/trianglengin_cpp.cp39-win_amd64.pyd +0 -0
- trianglengin/cpp/bindings.cpp +5 -8
- trianglengin/cpp/game_state.cpp +37 -55
- trianglengin/cpp/game_state.h +3 -1
- trianglengin/game_interface.py +8 -23
- trianglengin/trianglengin_cpp.cp310-win32.pyd +0 -0
- {trianglengin-2.0.6.dist-info → trianglengin-2.0.7.dist-info}/METADATA +128 -4
- {trianglengin-2.0.6.dist-info → trianglengin-2.0.7.dist-info}/RECORD +12 -12
- {trianglengin-2.0.6.dist-info → trianglengin-2.0.7.dist-info}/WHEEL +0 -0
- {trianglengin-2.0.6.dist-info → trianglengin-2.0.7.dist-info}/entry_points.txt +0 -0
- {trianglengin-2.0.6.dist-info → trianglengin-2.0.7.dist-info}/licenses/LICENSE +0 -0
- {trianglengin-2.0.6.dist-info → trianglengin-2.0.7.dist-info}/top_level.txt +0 -0
Binary file
|
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
|
@@ -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_),
|
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_),
|
28
|
-
grid_data_(other.grid_data_),
|
29
|
-
shapes_(other.shapes_),
|
30
|
-
score_(other.score_),
|
31
|
-
current_step_(other.current_step_),
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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_;
|
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_;
|
53
|
-
rng_ = other.rng_;
|
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
|
-
|
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;
|
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;
|
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();
|
178
|
-
get_valid_actions(true);
|
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
|
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;
|
193
|
+
score_ += reward;
|
195
194
|
|
196
|
-
return {reward, game_over_};
|
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>();
|
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);
|
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();
|
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;
|
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
|
|
@@ -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
|
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,18 +1,18 @@
|
|
1
1
|
trianglengin/__init__.py,sha256=VBYKMivxX19prPkxmSCSJJrjFXDVWkQJ9b5kQ6RzvkI,954
|
2
|
-
trianglengin/game_interface.py,sha256=
|
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=
|
5
|
-
trianglengin/Release/trianglengin_cpp.cp39-win_amd64.pyd,sha256=
|
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=
|
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=
|
15
|
-
trianglengin/cpp/game_state.h,sha256=
|
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.
|
55
|
-
trianglengin-2.0.
|
56
|
-
trianglengin-2.0.
|
57
|
-
trianglengin-2.0.
|
58
|
-
trianglengin-2.0.
|
59
|
-
trianglengin-2.0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|