392 lines
13 KiB
C++
392 lines
13 KiB
C++
#include "Game.h"
|
|
|
|
#include "GameBoard.h"
|
|
#include "GameParameters.h"
|
|
#include "Action.h"
|
|
|
|
#include <set>
|
|
#include <algorithm>
|
|
#include <memory>
|
|
|
|
static const int SUBPX_PER_ROW = 60; // the number of position the active piece can take "between" two rows
|
|
static const int LOCK_DELAY_SCORE = 1; // the score gained when the piece locks due to lock delay
|
|
static const int HARD_DROP_SCORE = 10; // the score gained when the piece locks due to an hard drop
|
|
static const int LINE_CLEAR_BASE_SCORE = 100; // the score value of clearing a single line
|
|
static const int B2B_SCORE_MULTIPLIER = 2; // by how much havaing B2B on multiplies the score of the line clear
|
|
static const int B2B_MIN_LINE_NUMBER = 4; // the minimum number of lines needed to be cleared at once to gain B2B (without a spin)
|
|
|
|
|
|
Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList) :
|
|
parameters(gamemode, controls),
|
|
board(boardWidth, boardHeight, piecesList, parameters.getNextQueueLength()) {
|
|
|
|
this->initialize();
|
|
}
|
|
|
|
void Game::start() {
|
|
this->started = true;
|
|
this->leftARETime = 1;
|
|
}
|
|
|
|
void Game::reset() {
|
|
this->initialize();
|
|
|
|
this->parameters.reset();
|
|
this->board.reset();
|
|
}
|
|
|
|
void Game::initialize() {
|
|
this->started = false;
|
|
this->lost = false;
|
|
|
|
this->score = 0;
|
|
this->framesPassed = 0;
|
|
this->B2BChain = 0;
|
|
|
|
this->heldActions.clear();
|
|
this->initialActions.clear();
|
|
this->heldDAS = 0;
|
|
this->heldARR = 0;
|
|
this->subVerticalPosition = 0;
|
|
this->totalLockDelay = 0;
|
|
this->totalForcedLockDelay = 0;
|
|
}
|
|
|
|
void Game::nextFrame(const std::set<Action>& playerActions) {
|
|
if (this->lost || this->hasWon()) return;
|
|
|
|
bool pieceJustLocked = false;
|
|
|
|
if (this->started) {
|
|
bool AREJustEnded = (this->leftARETime == 1);
|
|
if (this->leftARETime > 0) {
|
|
this->leftARETime--;
|
|
}
|
|
|
|
if (this->leftARETime == 0) {
|
|
if (AREJustEnded) {
|
|
this->lost = this->board.spawnNextPiece();
|
|
this->resetPiece(true);
|
|
|
|
/* IRS and IHS */
|
|
bool initialRotated = (this->initialActions.contains(ROTATE_0) || this->initialActions.contains(ROTATE_CW)
|
|
|| this->initialActions.contains(ROTATE_180) || this->initialActions.contains(ROTATE_CCW));
|
|
Rotation initialRotation = NONE
|
|
+ ((this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE)
|
|
+ ((this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE)
|
|
+ ((this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE);
|
|
|
|
if (this->initialActions.contains(HOLD)) {
|
|
this->board.hold(initialRotation);
|
|
}
|
|
else {
|
|
if (initialRotated) {
|
|
this->board.rotate(initialRotation);
|
|
}
|
|
}
|
|
|
|
this->lost = this->board.activePieceInWall();
|
|
if (this->lost) {
|
|
this->board.rotate(NONE);
|
|
this->lost = this->board.activePieceInWall();
|
|
|
|
if (this->lost) {
|
|
this->framesPassed++;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* HOLD */
|
|
if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) {
|
|
if (this->board.hold({})) {
|
|
this->resetPiece(false);
|
|
}
|
|
}
|
|
|
|
/* MOVE LEFT/RIGHT */
|
|
Position before = this->board.getActivePiecePosition();
|
|
|
|
if (this->heldDAS >= 0) {
|
|
if (playerActions.contains(MOVE_LEFT) && (!heldActions.contains(MOVE_LEFT))) this->movePiece(-1);
|
|
else if (playerActions.contains(MOVE_RIGHT)) this->movePiece(1);
|
|
else this->heldDAS = 0;
|
|
}
|
|
else if (this->heldDAS < 0) {
|
|
if (playerActions.contains(MOVE_RIGHT) && (!heldActions.contains(MOVE_RIGHT))) this->movePiece(1);
|
|
else if (playerActions.contains(MOVE_LEFT)) this->movePiece(-1);
|
|
else this->heldDAS = 0;
|
|
}
|
|
|
|
if (before != this->board.getActivePiecePosition()) {
|
|
this->totalLockDelay = 0;
|
|
}
|
|
|
|
/* ROTATIONS */
|
|
if (playerActions.contains(ROTATE_0) && (!this->heldActions.contains(ROTATE_0))) {
|
|
this->rotatePiece(NONE);
|
|
}
|
|
if (playerActions.contains(ROTATE_CW) && (!this->heldActions.contains(ROTATE_CW))) {
|
|
this->rotatePiece(CLOCKWISE);
|
|
}
|
|
if (playerActions.contains(ROTATE_180) && (!this->heldActions.contains(ROTATE_180))) {
|
|
this->rotatePiece(DOUBLE);
|
|
}
|
|
if (playerActions.contains(ROTATE_CCW) && (!this->heldActions.contains(ROTATE_CCW))) {
|
|
this->rotatePiece(COUNTERCLOCKWISE);
|
|
}
|
|
|
|
/* SOFT DROP */
|
|
if (playerActions.contains(SOFT_DROP)) {
|
|
int appliedSDR = this->parameters.getSDR();
|
|
|
|
// SDR=0 -> instant drop
|
|
if (appliedSDR == 0) {
|
|
while (this->board.moveDown());
|
|
}
|
|
// SDR>1 -> move down by specified amount
|
|
else {
|
|
this->subVerticalPosition += (SUBPX_PER_ROW / appliedSDR);
|
|
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
|
|
this->board.moveDown();
|
|
this->subVerticalPosition -= SUBPX_PER_ROW;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* HARD DROP */
|
|
// needs to be done last because we can enter ARE period afterwards
|
|
if (this->initialActions.contains(HARD_DROP) || (playerActions.contains(HARD_DROP) && (!this->heldActions.contains(HARD_DROP)))) {
|
|
while (this->board.moveDown());
|
|
this->lockPiece();
|
|
pieceJustLocked = true;
|
|
this->score += HARD_DROP_SCORE;
|
|
}
|
|
// no need to apply gravity and lock delay if the piece was hard dropped
|
|
else {
|
|
/* GRAVITY */
|
|
if (this->parameters.getLevel() >= 20) {
|
|
while (this->board.moveDown());
|
|
}
|
|
else {
|
|
// parameters.getGravity() gives the gravity for an assumed 20-line high board
|
|
int appliedGravity = this->parameters.getGravity() * std::max((double) this->board.getBoard().getBaseHeight() / 20.0, 1.0);
|
|
|
|
this->subVerticalPosition += appliedGravity;
|
|
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
|
|
this->subVerticalPosition -= SUBPX_PER_ROW;
|
|
this->board.moveDown();
|
|
}
|
|
}
|
|
|
|
/* LOCK DELAY */
|
|
if (this->board.touchesGround()) {
|
|
this->totalLockDelay++;
|
|
this->totalForcedLockDelay++;
|
|
}
|
|
else {
|
|
this->totalLockDelay = 0;
|
|
}
|
|
|
|
if ((this->totalLockDelay > this->parameters.getLockDelay()) || (this->totalForcedLockDelay > this->parameters.getForcedLockDelay())) {
|
|
this->lockPiece();
|
|
pieceJustLocked = true;
|
|
this->score += LOCK_DELAY_SCORE;
|
|
}
|
|
}
|
|
|
|
if (AREJustEnded) {
|
|
this->initialActions.clear();
|
|
}
|
|
}
|
|
|
|
this->framesPassed++;
|
|
if (this->lost) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((!this->started) || this->leftARETime > 0) {
|
|
for (Action action : playerActions) {
|
|
if ((!pieceJustLocked) && (!heldActions.contains(action))) {
|
|
this->initialActions.insert(action);
|
|
}
|
|
}
|
|
|
|
if (this->heldDAS >= 0) {
|
|
if (playerActions.contains(MOVE_LEFT)) this->heldDAS = -1;
|
|
else if (playerActions.contains(MOVE_RIGHT)) this->heldDAS++;
|
|
else this->heldDAS = 0;
|
|
}
|
|
else if (this->heldDAS < 0) {
|
|
if (playerActions.contains(MOVE_RIGHT)) this->heldDAS = +1;
|
|
else if (playerActions.contains(MOVE_LEFT)) this->heldDAS--;
|
|
else this->heldDAS = 0;
|
|
}
|
|
}
|
|
|
|
this->heldActions = playerActions;
|
|
}
|
|
|
|
void Game::resetPiece(bool newPiece) {
|
|
int appliedDAS = this->parameters.getDAS();
|
|
|
|
this->subVerticalPosition = 0;
|
|
this->totalLockDelay = 0;
|
|
if (newPiece) {
|
|
this->totalForcedLockDelay = 0;
|
|
}
|
|
if (abs(this->heldDAS) > appliedDAS) {
|
|
this->heldDAS = (this->heldDAS > 0) ? (+appliedDAS) : (-appliedDAS);
|
|
}
|
|
this->heldARR = 0;
|
|
}
|
|
|
|
void Game::movePiece(int movement) {
|
|
int appliedDAS = this->parameters.getDAS();
|
|
int appliedARR = this->parameters.getARR();
|
|
|
|
if ((this->heldDAS * movement) <= 0) {
|
|
this->heldDAS = movement;
|
|
this->heldARR = 0;
|
|
if (movement == -1) this->board.moveLeft();
|
|
if (movement == 1) this->board.moveRight();
|
|
}
|
|
else {
|
|
this->heldDAS += movement;
|
|
}
|
|
|
|
if (abs(this->heldDAS) > appliedDAS) {
|
|
// ARR=0 -> instant movement
|
|
if (appliedARR == 0) {
|
|
if (movement == -1) while (this->board.moveLeft());
|
|
if (movement == 1) while (this->board.moveRight());
|
|
}
|
|
|
|
// ARR>1 -> move by specified amount
|
|
else {
|
|
if (abs(this->heldDAS) > appliedDAS + 1) {
|
|
this->heldARR++;
|
|
}
|
|
if ((this->heldARR == appliedARR) || (abs(this->heldDAS) == (appliedDAS + 1))) {
|
|
this->heldARR = 0;
|
|
if (movement == -1) this->board.moveLeft();
|
|
if (movement == 1) this->board.moveRight();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Game::rotatePiece(Rotation rotation) {
|
|
Position before = this->board.getActivePiecePosition();
|
|
|
|
if (this->board.rotate(rotation)) {
|
|
this->totalLockDelay = 0;
|
|
if (before != this->board.getActivePiecePosition()) {
|
|
this->subVerticalPosition = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Game::lockPiece() {
|
|
LineClear clear = this->board.lockPiece();
|
|
this->parameters.lockedPiece(clear);
|
|
|
|
if (clear.lines > 0) {
|
|
bool B2BConditionsAreMet = ((clear.lines >= B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin);
|
|
|
|
/* clearing one more line is worth 2x more
|
|
clearing with a spin is worth as much as clearing 2x more lines */
|
|
long int clearScore = LINE_CLEAR_BASE_SCORE;
|
|
clearScore = clearScore << (clear.lines << (clear.isSpin));
|
|
|
|
if (this->B2BChain && B2BConditionsAreMet) {
|
|
clearScore *= B2B_SCORE_MULTIPLIER;
|
|
}
|
|
this->score += clearScore;
|
|
|
|
this->B2BChain = B2BConditionsAreMet;
|
|
}
|
|
|
|
if (!this->hasWon()) {
|
|
this->leftARETime = this->parameters.getARE();
|
|
if (this->leftARETime == 0) {
|
|
this->lost = this->board.spawnNextPiece();
|
|
this->resetPiece(true);
|
|
|
|
if (this->lost) {
|
|
this->board.rotate(NONE);
|
|
this->lost = this->board.activePieceInWall();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Game::hasWon() const {
|
|
return this->parameters.hasWon(this->framesPassed);
|
|
}
|
|
|
|
bool Game::hasLost() const {
|
|
return this->lost;
|
|
}
|
|
|
|
int Game::getClearedLines() const {
|
|
return this->parameters.getClearedLines();
|
|
}
|
|
|
|
int Game::getGrade() const {
|
|
return this->parameters.getGrade();
|
|
}
|
|
|
|
int Game::getLevel() const {
|
|
return this->parameters.getLevel();
|
|
}
|
|
|
|
int Game::getFramesPassed() const {
|
|
return this->framesPassed;
|
|
}
|
|
|
|
int Game::getScore() const {
|
|
return this->score;
|
|
}
|
|
|
|
bool Game::isOnB2BChain() const {
|
|
return this->B2BChain;
|
|
}
|
|
|
|
float Game::getLockDelayProgression() const {
|
|
return (float) this->totalLockDelay / this->parameters.getLockDelay();
|
|
}
|
|
|
|
float Game::getForcedLockDelayProgression() const {
|
|
return (float) this->totalForcedLockDelay / this->parameters.getForcedLockDelay();
|
|
}
|
|
|
|
bool Game::areBlocksBones() const {
|
|
return this->parameters.getBoneBlocks();
|
|
}
|
|
|
|
const Board& Game::getBoard() const {
|
|
return this->board.getBoard();
|
|
}
|
|
|
|
const std::shared_ptr<Piece>& Game::getActivePiece() const {
|
|
return this->board.getActivePiece();
|
|
}
|
|
|
|
const Position& Game::getActivePiecePosition() const {
|
|
return this->board.getActivePiecePosition();
|
|
}
|
|
|
|
Position Game::getGhostPiecePosition() const {
|
|
return this->board.lowestPosition();
|
|
}
|
|
|
|
const std::shared_ptr<Piece>& Game::getHeldPiece() const {
|
|
return this->board.getHeldPiece();
|
|
}
|
|
|
|
const std::vector<Piece>& Game::getNextPieces() const {
|
|
return this->board.getNextPieces();
|
|
}
|