fixed cooperate play
This commit is contained in:
317
src/gameplay/coop/CoopAIController.cpp
Normal file
317
src/gameplay/coop/CoopAIController.cpp
Normal file
@ -0,0 +1,317 @@
|
||||
#include "CoopAIController.h"
|
||||
|
||||
#include "CoopGame.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
|
||||
static bool canPlacePieceForSide(const std::array<CoopGame::Cell, CoopGame::COLS * CoopGame::ROWS>& 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<CoopGame::Cell, CoopGame::COLS * CoopGame::ROWS>& 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<uint8_t, CoopGame::COLS * CoopGame::ROWS>& 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<double>::infinity();
|
||||
int rot = 0;
|
||||
int x = 10;
|
||||
};
|
||||
|
||||
static Eval evaluateBestPlacementForSide(const CoopGame& game, CoopGame::PlayerSide side) {
|
||||
const auto& board = game.boardRef();
|
||||
|
||||
std::array<uint8_t, CoopGame::COLS * CoopGame::ROWS> 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<int, 10> 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<double>(fullRows) * 12000.0;
|
||||
// Heavily prefer completing the player's half — make this a primary objective.
|
||||
s += static_cast<double>(sideHalfFullRows) * 6000.0;
|
||||
// Penalize holes and height less aggressively so completing half-rows is prioritized.
|
||||
s -= static_cast<double>(holes) * 180.0;
|
||||
s -= static_cast<double>(aggregateHeight) * 4.0;
|
||||
s -= static_cast<double>(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user