Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.6 5b2cf908d1 Fix attacker/defender starting position overlap on Motcia map
Motcia's attacker starting position index 0 had 7 of 10 positions
overlapping with defender starting positions. This caused the AI's
GameStateGuesser to place a guessed defender unit on an attacker
starting position during SET_UP, blocking it and creating a command
count mismatch (27 vs 30) that crashed the server.

Moved attacker index 0 positions east into the plains (columns 3-5)
away from the defender's castle compound at (1,0)/(2,0)/(2,1).

Also adds a CI test that validates no map has attacker starting
positions that overlap with defender starting positions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:45:17 -08:00
adminandClaude Opus 4.6 496545b006 Add detailed coords and occupant logging to AI command mismatch diagnostic
The guessed state produces 27 PLACE_UNIT_COMMANDs vs 30 real during SET_UP.
This adds target coordinates to the command dump, identifies the specific
missing positions per unit, checks what the guessed state thinks is occupying
them, and dumps all guessed state units for full visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:20:11 -08:00
3 changed files with 134 additions and 35 deletions
@@ -20,6 +20,9 @@
#include <sstream>
#endif
#include <set>
#include <unordered_map>
#include "AIAttackerStrategySelector.hpp"
#include "AIConfig.hpp"
#include "AIDefenderStrategySelector.hpp"
@@ -197,13 +200,85 @@ auto ShardokAIClient::StandardChooseCommandIndex(
: -1;
const int guessedActor =
i < commandCount ? (*guessedCommands)[i]->GetActorUnitId() : -1;
const int realRow = i < realAvailableCommands->size()
? (*realAvailableCommands)[i]->GetTargetRow()
: -1;
const int realCol = i < realAvailableCommands->size()
? (*realAvailableCommands)[i]->GetTargetColumn()
: -1;
const int guessedRow = i < commandCount ? (*guessedCommands)[i]->GetTargetRow() : -1;
const int guessedCol = i < commandCount ? (*guessedCommands)[i]->GetTargetColumn() : -1;
fprintf(stderr,
" [%zu] real: %s unit=%d guessed: %s unit=%d\n",
" [%zu] real: %s unit=%d (%d,%d) guessed: %s unit=%d (%d,%d)\n",
i,
realName,
realActor,
realRow,
realCol,
guessedName,
guessedActor);
guessedActor,
guessedRow,
guessedCol);
}
// Find positions in real commands that are missing from guessed commands
// Group by unit to find the missing position per unit
std::unordered_map<int, std::set<std::pair<int, int>>> realPositions;
std::unordered_map<int, std::set<std::pair<int, int>>> guessedPositions;
for (size_t i = 0; i < realAvailableCommands->size(); i++) {
const auto &cmd = (*realAvailableCommands)[i];
realPositions[cmd->GetActorUnitId()].emplace(
cmd->GetTargetRow(),
cmd->GetTargetColumn());
}
for (size_t i = 0; i < commandCount; i++) {
const auto &cmd = (*guessedCommands)[i];
guessedPositions[cmd->GetActorUnitId()].emplace(
cmd->GetTargetRow(),
cmd->GetTargetColumn());
}
for (const auto &[unitId, positions] : realPositions) {
for (const auto &[row, col] : positions) {
if (!guessedPositions[unitId].contains({row, col})) {
fprintf(stderr,
" Missing from guessed: unit=%d pos=(%d,%d)",
unitId,
row,
col);
const Coords missingCoords(row, col);
const auto *occupant = guessedState.GetOccupant(missingCoords);
if (occupant) {
fprintf(stderr,
" -> guessed occupant: unit_id=%d player=%d status=%d "
"loc=(%d,%d) hidden=%d battalion_size=%d\n",
occupant->unit_id(),
occupant->player_id(),
static_cast<int>(occupant->status()),
occupant->location().row(),
occupant->location().column(),
occupant->hidden(),
occupant->battalion().size());
} else {
fprintf(stderr, " -> no occupant in guessed state\n");
}
}
}
}
// Dump all guessed state units for full picture
fprintf(stderr, " Guessed state units (%u total):\n", guessedState->units()->size());
for (size_t i = 0; i < guessedState->units()->size(); i++) {
const auto *u = guessedState->units()->Get(static_cast<unsigned int>(i));
fprintf(stderr,
" unit_id=%d player=%d status=%d loc=(%d,%d) hidden=%d "
"starting_pos_idx=%d\n",
u->unit_id(),
u->player_id(),
static_cast<int>(u->status()),
u->location().row(),
u->location().column(),
u->hidden(),
u->starting_position_index());
}
throw ShardokInternalErrorException(
@@ -580,45 +580,45 @@
"attackerStartingPositions": [
{
"positions": [
{
"row": 0,
"column": 0
},
{
"row": 0,
"column": 1
},
{
"row": 0,
"column": 2
},
{
"row": 1,
"column": 0
},
{
"row": 1,
"column": 1
},
{
"row": 1,
"column": 2
},
{
"row": 2,
"column": 0
},
{
"row": 2,
"column": 1
},
{
"row": 0,
"column": 3
},
{
"row": 0,
"column": 4
},
{
"row": 0,
"column": 5
},
{
"row": 1,
"column": 3
},
{
"row": 1,
"column": 4
},
{
"row": 1,
"column": 5
},
{
"row": 2,
"column": 3
},
{
"row": 2,
"column": 4
},
{
"row": 3,
"column": 0
"column": 2
},
{
"row": 3,
"column": 3
}
]
},
@@ -7,8 +7,10 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <set>
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GameSettings_test_utils.hpp"
using std::vector;
@@ -74,4 +76,26 @@ TEST_F(MapInfoCalculatorTest, yuetia_checkStartingPositions) {
{{0, 0}, {1, 0}, {3, 0}, {4, 0}, {5, 0}, {6, 0}, {7, 0}});
EXPECT_EQ(expectedRequirements, yuetiaInfo.positionsRequiringCrossing);
}
TEST_F(MapInfoCalculatorTest, noAttackerDefenderStartingPositionOverlap) {
for (const auto& mapName : GetMapNames()) {
const auto mapProto = LoadMap(mapName);
// Collect defender starting positions
std::set<std::pair<int, int>> defenderPositions;
for (const auto& pos : mapProto.defender_starting_positions().positions()) {
defenderPositions.emplace(pos.row(), pos.column());
}
// Check each attacker starting position list for overlap
for (int idx = 0; idx < mapProto.attacker_starting_positions_size(); idx++) {
const auto& attackerList = mapProto.attacker_starting_positions(idx);
for (const auto& pos : attackerList.positions()) {
EXPECT_FALSE(defenderPositions.contains({pos.row(), pos.column()}))
<< mapName << ": attacker index " << idx << " position (" << pos.row()
<< "," << pos.column() << ") overlaps with a defender starting position";
}
}
}
}