#include "CoopAIController.h" #include "CoopGame.h" #include #include #include #include namespace { static bool canPlacePieceForSide(const std::array& board, const CoopGame::Piece& p, CoopGame::PlayerSide side) { for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (!CoopGame::cellFilled(p, cx, cy)) { continue; } const int bx = p.x + cx; const int by = p.y + cy; // Keep the AI strictly in the correct half. if (side == CoopGame::PlayerSide::Right) { if (bx < 10 || bx >= CoopGame::COLS) { return false; } } else { if (bx < 0 || bx >= 10) { return false; } } // Above the visible board is allowed. if (by < 0) { continue; } if (by >= CoopGame::ROWS) { return false; } if (board[by * CoopGame::COLS + bx].occupied) { return false; } } } return true; } static int dropYFor(const std::array& board, CoopGame::Piece p, CoopGame::PlayerSide side) { // Assumes p is currently placeable. while (true) { CoopGame::Piece next = p; next.y += 1; if (!canPlacePieceForSide(board, next, side)) { return p.y; } p = next; if (p.y > CoopGame::ROWS) { return p.y; } } } static void applyPiece(std::array& occ, const CoopGame::Piece& p) { for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (!CoopGame::cellFilled(p, cx, cy)) { continue; } const int bx = p.x + cx; const int by = p.y + cy; if (by < 0 || by >= CoopGame::ROWS || bx < 0 || bx >= CoopGame::COLS) { continue; } occ[by * CoopGame::COLS + bx] = 1; } } } struct Eval { double score = -std::numeric_limits::infinity(); int rot = 0; int x = 10; }; static Eval evaluateBestPlacementForSide(const CoopGame& game, CoopGame::PlayerSide side) { const auto& board = game.boardRef(); std::array occ{}; for (int i = 0; i < CoopGame::COLS * CoopGame::ROWS; ++i) { occ[i] = board[i].occupied ? 1 : 0; } const CoopGame::Piece cur = game.current(side); Eval best{}; // Iterate rotations and x positions. IMPORTANT: allow x to go slightly out of bounds // because our pieces are represented in a 4x4 mask and many rotations have leading // empty columns. For example, placing a vertical I/J/L into column 0 often requires // p.x == -1 or p.x == -2 so the filled cells land at bx==0. // canPlacePieceForSide() enforces the actual half-board bounds. for (int rot = 0; rot < 4; ++rot) { int xmin = (side == CoopGame::PlayerSide::Right) ? 6 : -3; int xmax = (side == CoopGame::PlayerSide::Right) ? 22 : 13; for (int x = xmin; x <= xmax; ++x) { CoopGame::Piece p = cur; p.rot = rot; p.x = x; // If this rotation/x is illegal at the current y, try near the top spawn band. if (!canPlacePieceForSide(board, p, side)) { p.y = -2; if (!canPlacePieceForSide(board, p, side)) { continue; } } p.y = dropYFor(board, p, side); auto occ2 = occ; applyPiece(occ2, p); // Count completed full rows (all 20 cols) after placement. int fullRows = 0; for (int y = 0; y < CoopGame::ROWS; ++y) { bool full = true; for (int cx = 0; cx < CoopGame::COLS; ++cx) { if (!occ2[y * CoopGame::COLS + cx]) { full = false; break; } } if (full) { ++fullRows; } } // Right-half column heights + holes + bumpiness. std::array heights{}; int aggregateHeight = 0; int holes = 0; for (int c = 0; c < 10; ++c) { const int bx = (side == CoopGame::PlayerSide::Right) ? (10 + c) : c; int h = 0; bool found = false; for (int y = 0; y < CoopGame::ROWS; ++y) { if (occ2[y * CoopGame::COLS + bx]) { h = CoopGame::ROWS - y; found = true; // Count holes below the first filled cell. for (int yy = y + 1; yy < CoopGame::ROWS; ++yy) { if (!occ2[yy * CoopGame::COLS + bx]) { ++holes; } } break; } } heights[c] = found ? h : 0; aggregateHeight += heights[c]; } int bump = 0; for (int i = 0; i < 9; ++i) { bump += std::abs(heights[i] - heights[i + 1]); } // Reward sync potential: rows where the right half is full (10..19). int sideHalfFullRows = 0; for (int y = 0; y < CoopGame::ROWS; ++y) { bool full = true; int start = (side == CoopGame::PlayerSide::Right) ? 10 : 0; int end = (side == CoopGame::PlayerSide::Right) ? 20 : 10; for (int bx = start; bx < end; ++bx) { if (!occ2[y * CoopGame::COLS + bx]) { full = false; break; } } if (full) { ++sideHalfFullRows; } } // Simple heuristic: // - Strongly prefer completed full rows // - Prefer making the right half complete (helps cooperative clears) // - Penalize holes and excessive height/bumpiness double s = 0.0; // Strongly prefer full-line clears across the whole board (rare but best). s += static_cast(fullRows) * 12000.0; // Heavily prefer completing the player's half — make this a primary objective. s += static_cast(sideHalfFullRows) * 6000.0; // Penalize holes and height less aggressively so completing half-rows is prioritized. s -= static_cast(holes) * 180.0; s -= static_cast(aggregateHeight) * 4.0; s -= static_cast(bump) * 10.0; // Reduce center bias so edge placements to complete rows are not punished. double centerTarget = (side == CoopGame::PlayerSide::Right) ? 15.0 : 4.5; const double centerBias = -std::abs((x + 1.5) - centerTarget) * 1.0; s += centerBias; if (s > best.score) { best.score = s; best.rot = rot; best.x = x; } } } return best; } } // namespace void CoopAIController::reset() { m_lastPieceSeq = 0; m_hasPlan = false; m_targetRot = 0; m_targetX = 10; m_moveTimerMs = 0.0; m_moveDir = 0; m_rotateTimerMs = 0.0; } void CoopAIController::computePlan(const CoopGame& game, CoopGame::PlayerSide side) { const Eval best = evaluateBestPlacementForSide(game, side); m_targetRot = best.rot; m_targetX = best.x; m_hasPlan = true; m_moveTimerMs = 0.0; m_moveDir = 0; m_rotateTimerMs = 0.0; } void CoopAIController::update(CoopGame& game, CoopGame::PlayerSide side, double frameMs) { const uint64_t seq = game.currentPieceSequence(side); if (seq != m_lastPieceSeq) { m_lastPieceSeq = seq; m_hasPlan = false; m_moveTimerMs = 0.0; m_moveDir = 0; m_rotateTimerMs = 0.0; } if (!m_hasPlan) { computePlan(game, side); } const CoopGame::Piece cur = game.current(side); // Clamp negative deltas (defensive; callers should pass >= 0). const double dt = std::max(0.0, frameMs); // Update timers. if (m_moveTimerMs > 0.0) { m_moveTimerMs -= dt; if (m_moveTimerMs < 0.0) m_moveTimerMs = 0.0; } if (m_rotateTimerMs > 0.0) { m_rotateTimerMs -= dt; if (m_rotateTimerMs < 0.0) m_rotateTimerMs = 0.0; } // Rotate toward target first. const int curRot = ((cur.rot % 4) + 4) % 4; const int tgtRot = ((m_targetRot % 4) + 4) % 4; int diff = (tgtRot - curRot + 4) % 4; if (diff != 0) { // Human-ish rotation rate limiting. if (m_rotateTimerMs <= 0.0) { const int dir = (diff == 3) ? -1 : 1; game.rotate(side, dir); m_rotateTimerMs = m_rotateIntervalMs; } // While rotating, do not also slide horizontally in the same frame. m_moveDir = 0; m_moveTimerMs = 0.0; return; } // Move horizontally toward target. int desiredDir = 0; if (cur.x < m_targetX) desiredDir = +1; else if (cur.x > m_targetX) desiredDir = -1; if (desiredDir == 0) { // Aligned: do nothing. Gravity controls fall speed (no AI hard drops). m_moveDir = 0; m_moveTimerMs = 0.0; return; } // DAS/ARR-style horizontal movement pacing. if (m_moveDir != desiredDir) { // New direction / initial press: move immediately, then wait DAS. game.move(side, desiredDir); m_moveDir = desiredDir; m_moveTimerMs = m_dasMs; return; } // Holding direction: repeat every ARR once DAS has elapsed. if (m_moveTimerMs <= 0.0) { game.move(side, desiredDir); m_moveTimerMs = m_arrMs; } }