fix(@projects/@magic-civilization): 🐛 fix axial tile iteration and coordinate handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
5753d0d4ab
commit
c3b1ae550e
2 changed files with 60 additions and 11 deletions
|
|
@ -118,25 +118,32 @@ static func build_tactical_state(focal: RefCounted) -> Dictionary:
|
|||
## Build the full tiles JSON array for GdAiController::set_map().
|
||||
## Called once at game-start to populate the Rust-resident tile catalog.
|
||||
## Returns a JSON string (Array of TacticalTile objects).
|
||||
##
|
||||
## CRITICAL: `game_map.tiles` is an axial-keyed dict — `r` is legitimately
|
||||
## negative across roughly half the map (parallelogram, not rectangle). We
|
||||
## MUST iterate the dict directly. An offset-rectangle `for row in range(h)`
|
||||
## loop misses real axial tiles like `(25, -2)` AND mis-labels every cached
|
||||
## tile (the `[col, row]` pair gets stored in the `hex` field, but
|
||||
## `update_tile` later searches by axial `tile.position` and never matches).
|
||||
## See `mc-ai/src/tactical/settle.rs:tile_at` for the consumer-side contract.
|
||||
static func build_tactical_tiles_json(game_map: RefCounted) -> String:
|
||||
if game_map == null:
|
||||
return "[]"
|
||||
var width: int = int(game_map.width)
|
||||
var height: int = int(game_map.height)
|
||||
var tiles: Array = []
|
||||
for row: int in range(height):
|
||||
for col: int in range(width):
|
||||
tiles.append(tile_to_dict(game_map, col, row))
|
||||
for axial: Vector2i in game_map.tiles.keys():
|
||||
tiles.append(tile_to_dict(game_map, axial.x, axial.y))
|
||||
return JSON.stringify(tiles)
|
||||
|
||||
|
||||
## Build a single tile JSON string for GdAiController::update_tile().
|
||||
## Called when a tile mutates (improvement built, border expanded, etc.).
|
||||
static func build_single_tile_json(col: int, row: int) -> String:
|
||||
## `q`, `r` are axial coordinates — matches the `tile.position` axial pair
|
||||
## emitted by every EventBus signal that drives this path.
|
||||
static func build_single_tile_json(q: int, r: int) -> String:
|
||||
var game_map: RefCounted = GameState.get_game_map()
|
||||
if game_map == null:
|
||||
return ""
|
||||
return JSON.stringify(tile_to_dict(game_map, col, row))
|
||||
return JSON.stringify(tile_to_dict(game_map, q, r))
|
||||
|
||||
|
||||
static func build_unit_catalog() -> Array:
|
||||
|
|
@ -298,17 +305,21 @@ static func dict_string_field(entry: Dictionary, key: String) -> String:
|
|||
return ""
|
||||
|
||||
|
||||
static func tile_to_dict(game_map: RefCounted, col: int, row: int) -> Dictionary:
|
||||
var tile: Resource = game_map.get_tile(Vector2i(col, row))
|
||||
## `q`, `r` are axial coordinates. `game_map.get_tile` expects axial and
|
||||
## `tiles` is axial-keyed; the emitted `hex` field is the axial pair so
|
||||
## `update_tile`'s linear-search by `hex` (`mc-ai::tactical::settle::tile_at`)
|
||||
## can locate the tile after later mutations.
|
||||
static func tile_to_dict(game_map: RefCounted, q: int, r: int) -> Dictionary:
|
||||
var tile: Resource = game_map.get_tile(Vector2i(q, r))
|
||||
if tile == null:
|
||||
return {
|
||||
"hex": [col, row], "biome": "", "yields": [0, 0, 0],
|
||||
"hex": [q, r], "biome": "", "yields": [0, 0, 0],
|
||||
"resource": null, "is_coast": false, "owner": null,
|
||||
}
|
||||
var yields: Dictionary = tile.get_yields(-1)
|
||||
var resource: String = String(tile.resource_id)
|
||||
return {
|
||||
"hex": [col, row],
|
||||
"hex": [q, r],
|
||||
"biome": String(tile.biome_id),
|
||||
"yields": [
|
||||
maxi(0, int(yields.get("food", 0))),
|
||||
|
|
|
|||
|
|
@ -959,6 +959,44 @@ mod tests {
|
|||
assert!(!replace_by_hex(&mut map, (99, 99), tile_with((99, 99), "x")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_map_json_roundtrip_preserves_negative_axial_r() {
|
||||
// Regression — p2-10l-followup-gdai-set-map:
|
||||
// Production failure mode was a 960-tile cache where `(25, -2)` could
|
||||
// not be located after `update_tile`. Root cause: the GDScript
|
||||
// producer iterated an offset rectangle and wrote `[col, row]` into
|
||||
// the `hex` field, so legitimate negative-axial-r tiles were both
|
||||
// absent from the cache AND mis-labelled.
|
||||
//
|
||||
// This test pins the Rust side of the contract: when the JSON tile
|
||||
// array carries axial coords (including negative `r`), the cache
|
||||
// round-trips them faithfully and a subsequent `update_tile`-style
|
||||
// linear search by `hex` field locates the tile.
|
||||
let tiles_json = r#"[
|
||||
{"hex":[0,0],"biome":"plains","yields":[2,1,1],"resource":null,"is_coast":false,"owner":null},
|
||||
{"hex":[25,-2],"biome":"hills","yields":[1,2,0],"resource":null,"is_coast":false,"owner":null},
|
||||
{"hex":[13,-7],"biome":"forest","yields":[1,1,0],"resource":null,"is_coast":false,"owner":null},
|
||||
{"hex":[-3,-1],"biome":"tundra","yields":[1,0,0],"resource":null,"is_coast":false,"owner":null}
|
||||
]"#;
|
||||
let parsed: Vec<TacticalTile> =
|
||||
serde_json::from_str(tiles_json).expect("axial JSON parses");
|
||||
let map = TacticalMap { width: 40, height: 24, tiles: parsed };
|
||||
|
||||
// `update_tile`'s key contract: `tiles.iter().find(|t| t.hex == key)`.
|
||||
// Negative axial r must be findable.
|
||||
let hit = map.tiles.iter().find(|t| t.hex == (25, -2));
|
||||
assert!(hit.is_some(), "(25, -2) must be locatable after JSON round-trip");
|
||||
assert_eq!(hit.unwrap().biome, "hills");
|
||||
|
||||
assert!(map.tiles.iter().find(|t| t.hex == (13, -7)).is_some());
|
||||
assert!(map.tiles.iter().find(|t| t.hex == (-3, -1)).is_some());
|
||||
|
||||
// Sanity: offset-rectangle pairs that would have been emitted by the
|
||||
// buggy producer must NOT match — proves the test detects the
|
||||
// regression rather than passing trivially.
|
||||
assert!(map.tiles.iter().find(|t| t.hex == (25, 22)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stats_payload_for_emits_canonical_dict_shape() {
|
||||
let json = stats_payload_for("Attack");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue