paulo@0: /* Engine of LOCKJAW, an implementation of the Soviet Mind Game paulo@0: paulo@0: Copyright (C) 2006 Damian Yerrick paulo@0: paulo@0: This work is free software; you can redistribute it and/or modify paulo@0: it under the terms of the GNU General Public License as published by paulo@0: the Free Software Foundation; either version 2 of the License, or paulo@0: (at your option) any later version. paulo@0: paulo@0: This program is distributed in the hope that it will be useful, paulo@0: but WITHOUT ANY WARRANTY; without even the implied warranty of paulo@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the paulo@0: GNU General Public License for more details. paulo@0: paulo@0: You should have received a copy of the GNU General Public License paulo@0: along with this program; if not, write to the Free Software paulo@0: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA paulo@0: paulo@0: Original game concept and design by Alexey Pajitnov. paulo@0: The Software is not sponsored or endorsed by Alexey Pajitnov, Elorg, paulo@0: or The Tetris Company LLC. paulo@0: paulo@0: paulo@0: */ paulo@0: paulo@0: #define LJ_INTERNAL paulo@0: #include "lj.h" paulo@0: paulo@0: unsigned int ljRand(LJField *p) { paulo@0: p->seed = p->seed * 2147001325 + 715136305; paulo@0: return p->seed >> 17; paulo@0: } paulo@0: paulo@0: static inline void ljAssert(LJField *p, int shouldBeTrue, const char *reason) { paulo@0: if (!shouldBeTrue) { paulo@0: p->state = LJS_GAMEOVER; paulo@0: } paulo@0: } paulo@0: paulo@0: static const char xShapes[N_PIECE_SHAPES][4][4] = { paulo@0: { {0,1,2,3}, {2,2,2,2}, {3,2,1,0}, {1,1,1,1} }, // I paulo@0: { {0,1,2,0}, {1,1,1,2}, {2,1,0,2}, {1,1,1,0} }, // J paulo@0: { {0,1,2,2}, {1,1,1,2}, {2,1,0,0}, {1,1,1,0} }, // L paulo@0: { {1,1,2,2}, {1,2,2,1}, {2,2,1,1}, {2,1,1,2} }, // O paulo@0: { {0,1,1,2}, {1,1,2,2}, {2,1,1,0}, {1,1,0,0} }, // S paulo@0: { {0,1,2,1}, {1,1,1,2}, {2,1,0,1}, {1,1,1,0} }, // T paulo@0: { {0,1,1,2}, {2,2,1,1}, {2,1,1,0}, {0,0,1,1} }, // Z paulo@0: { {0,1,2,3}, {2,2,2,2}, {3,2,1,0}, {1,1,1,1} }, // I2 paulo@0: { {0,1,2,0}, {1,1,1,2}, {2,1,0,2}, {1,1,1,0} }, // I3 paulo@0: { {1,1,2,2}, {1,2,2,1}, {2,2,1,1}, {2,1,1,2} }, // L3 paulo@0: }; paulo@0: paulo@0: static const char yShapes[N_PIECE_SHAPES][4][4] = { paulo@0: { {2,2,2,2}, {3,2,1,0}, {1,1,1,1}, {0,1,2,3} }, // I paulo@0: { {2,2,2,3}, {3,2,1,3}, {2,2,2,1}, {1,2,3,1} }, // J paulo@0: { {2,2,2,3}, {3,2,1,1}, {2,2,2,1}, {1,2,3,3} }, // L paulo@0: { {2,3,3,2}, {3,3,2,2}, {3,2,2,3}, {2,2,3,3} }, // O paulo@0: { {2,2,3,3}, {3,2,2,1}, {2,2,1,1}, {1,2,2,3} }, // S paulo@0: { {2,2,2,3}, {3,2,1,2}, {2,2,2,1}, {1,2,3,2} }, // T paulo@0: { {3,3,2,2}, {3,2,2,1}, {1,1,2,2}, {1,2,2,3} }, // Z paulo@0: { {2,2,2,2}, {3,2,1,0}, {1,1,1,1}, {0,1,2,3} }, // I2 paulo@0: { {2,2,2,3}, {3,2,1,3}, {2,2,2,1}, {1,2,3,1} }, // I3 paulo@0: { {2,3,3,2}, {3,3,2,2}, {3,2,2,3}, {2,2,3,3} }, // L3 paulo@0: }; paulo@0: paulo@0: const char pieceColors[N_PIECE_SHAPES] = { paulo@0: 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, paulo@0: 0x40, 0x10, 0x20 paulo@0: }; paulo@0: paulo@0: static const signed char connShapes[N_PIECE_SHAPES][4] = { paulo@0: {CONNECT_R, CONNECT_LR, CONNECT_LR, CONNECT_L}, // I paulo@0: {CONNECT_UR, CONNECT_LR, CONNECT_L, CONNECT_D}, // J paulo@0: {CONNECT_R, CONNECT_LR, CONNECT_UL, CONNECT_D}, // L paulo@0: {CONNECT_UR, CONNECT_DR, CONNECT_DL, CONNECT_UL}, // O paulo@0: {CONNECT_R, CONNECT_UL, CONNECT_DR, CONNECT_L}, // S paulo@0: {CONNECT_R, CONNECT_ULR, CONNECT_L, CONNECT_D}, // T paulo@0: {CONNECT_R, CONNECT_DL, CONNECT_UR, CONNECT_L}, // Z paulo@0: {-1, CONNECT_R, CONNECT_L, -1}, // I2 paulo@0: {CONNECT_R, CONNECT_LR, CONNECT_L, -1}, // I3 paulo@0: {CONNECT_UR, CONNECT_D, -1, CONNECT_L}, // L3 paulo@0: }; paulo@0: paulo@0: static inline int pieceToFieldBlock(int piece, int conn) { paulo@0: return conn | pieceColors[piece]; paulo@0: } paulo@0: paulo@0: /** paulo@0: * @theta rotation state: 0-3 normal, 4 for next piece paulo@0: */ paulo@0: void expandPieceToBlocks(LJBlkSpec out[], paulo@0: const LJField *p, int piece, int xBase, int yBase, int theta) { paulo@0: int shape = piece & LJP_MASK; paulo@0: paulo@0: if (theta >= 4) { paulo@0: const LJRotSystem *rs = rotSystems[p->rotationSystem]; paulo@0: int kickData = rs->entryOffset[shape]; paulo@0: xBase += WKX(kickData); paulo@0: yBase += WKY(kickData); paulo@0: kickData = rs->entryOffset[LJP_I]; paulo@0: xBase -= WKX(kickData); paulo@0: yBase -= WKY(kickData); paulo@0: theta = rs->entryTheta[shape]; paulo@0: } paulo@0: paulo@0: const char *xBl = xShapes[shape][theta]; paulo@0: const char *yBl = yShapes[shape][theta]; paulo@0: paulo@0: for (int blk = 0; blk < 4; ++blk) { paulo@0: if (connShapes[shape][blk] == -1) { paulo@0: out[blk].conn = 0; paulo@0: } else { paulo@0: int conn = connShapes[shape][blk] << theta; paulo@0: int connRotated = (conn | (conn >> 4)) & CONNECT_MASK; paulo@0: int color = ((0x10 << blk) & piece) ? 8 : piece; paulo@0: out[blk].y = yBl[blk] + yBase; paulo@0: out[blk].x = xBl[blk] + xBase; paulo@0: out[blk].conn = pieceToFieldBlock(color, connRotated); paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: int isOccupied(const LJField *p, int x, int y) { paulo@0: if (x < p->leftWall || x >= p->rightWall || y < 0) paulo@0: return 1; paulo@0: if (y > LJ_PF_HT) paulo@0: return 0; paulo@0: return p->b[y][x] > 1; paulo@0: } paulo@0: paulo@0: int isCollision(const LJField *p, int x, int y, int theta) { paulo@0: LJBlkSpec blocks[4]; paulo@0: int piece = p->curPiece[0]; paulo@0: paulo@0: expandPieceToBlocks(blocks, p, piece, x, y, theta); paulo@0: paulo@0: for (int blk = 0; blk < 4; ++blk) { paulo@0: if (blocks[blk].conn paulo@0: && isOccupied(p, blocks[blk].x, blocks[blk].y)) paulo@0: return 1; paulo@0: } paulo@0: return 0; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Determines whether the piece that just landed was a T-spin. paulo@0: * Must be called just BEFORE lockdown writes the blocks to the paulo@0: * playfield; otherwise TNT will break. paulo@0: */ paulo@0: static int isTspin(const LJField *p) { paulo@0: int blks = 0; paulo@0: int x = p->x; paulo@0: int y = p->hardDropY; paulo@0: paulo@0: switch (p->tSpinAlgo) { paulo@0: case LJTS_TNT: paulo@0: if (!isCollision(p, x, y + 1, p->theta) paulo@0: || !isCollision(p, x - 1, y, p->theta) paulo@0: || !isCollision(p, x + 1, y, p->theta)) { paulo@0: return 0; paulo@0: } paulo@0: return p->isSpin; paulo@0: case LJTS_TDS_NO_KICK: paulo@0: paulo@0: // If t-spin involved wall kick, don't count it paulo@0: if (p->isSpin == 2) { paulo@0: return 0; paulo@0: } paulo@0: paulo@0: // otherwise fall through paulo@0: case LJTS_TDS: paulo@0: // 1. T tetromino paulo@0: if ((p->curPiece[0] & LJP_MASK) != LJP_T) { paulo@0: return 0; paulo@0: } paulo@0: paulo@0: // 2. Last move was spin paulo@0: if (!p->isSpin) { paulo@0: return 0; paulo@0: } paulo@0: paulo@0: // 3. At least three cells around the rotation center are full paulo@0: if (isOccupied(p, x, y + 1)) { paulo@0: ++blks; paulo@0: } paulo@0: if (isOccupied(p, x, y + 3)) { paulo@0: ++blks; paulo@0: } paulo@0: if (isOccupied(p, x + 2, y + 1)) { paulo@0: ++blks; paulo@0: } paulo@0: if (isOccupied(p, x + 2, y + 3)) { paulo@0: ++blks; paulo@0: } paulo@0: if (blks < 3) { paulo@0: return 0; paulo@0: } paulo@0: paulo@0: // 3. Last move was spin paulo@0: return p->isSpin; paulo@0: default: paulo@0: return 0; paulo@0: } paulo@0: } paulo@0: paulo@0: /** paulo@0: * Calculates where the active piece in a playfield will fall paulo@0: * if dropped, and writes it back to the playfield. paulo@0: * This value is used for ghost piece, gravity, soft drop, and paulo@0: * hard drop. Call this after the active piece has been spawned, paulo@0: * moved, or rotated. paulo@0: * @param p the playfield paulo@0: */ paulo@0: static void updHardDropY(LJField *p) { paulo@0: int x = p->x; paulo@0: int y = ljfixfloor(p->y); paulo@0: int theta = p->theta; paulo@0: paulo@0: if (p->bottomBlocks) { paulo@0: y = -2; paulo@0: while (y < (int)LJ_PF_HT paulo@0: && y < ljfixfloor(p->y) paulo@0: && isCollision(p, x, y, theta)) { paulo@0: ++y; paulo@0: } paulo@0: } paulo@0: else { paulo@0: while (!isCollision(p, x, y - 1, theta)) { paulo@0: --y; paulo@0: } paulo@0: } paulo@0: p->hardDropY = y; paulo@0: } paulo@0: paulo@0: paulo@0: /** paulo@0: * Look for a TNT square in this position. paulo@0: * @param x column of left side, such that 0 <= x <= playfield width - 4 paulo@0: * @param y row of bottom block, such that 0 <= y <= playfield height - 4 paulo@0: * @param isMulti nonzero for multisquares; 0 for monosquares paulo@0: * @return nonzero if found; 0 if not found paulo@0: */ paulo@0: static int isSquareAt(LJField *p, int x, int y, int isMulti) paulo@0: { paulo@0: int firstColor = p->b[y][x] & COLOR_MASK; paulo@0: paulo@0: // Check the frame to make sure it isn't connected to anything else paulo@0: for(int i = 0; i <= 3; i++) paulo@0: { paulo@0: /* don't allow squares within parts of squares */ paulo@0: if((p->b[y + i][x] & COLOR_MASK) >= 0x80) paulo@0: return 0; paulo@0: /* the block doesn't connect on the left */ paulo@0: if(p->b[y + i][x] & CONNECT_L) paulo@0: return 0; paulo@0: /* the block doesn't connect on the right */ paulo@0: if(p->b[y + i][x + 3] & CONNECT_R) paulo@0: return 0; paulo@0: /* the block doesn't connect on the bottom */ paulo@0: if(p->b[y][x + i] & CONNECT_D) paulo@0: return 0; paulo@0: /* the block doesn't connect on the top */ paulo@0: if(p->b[y + 3][x + i] & CONNECT_U) paulo@0: return 0; paulo@0: } paulo@0: paulo@0: for(int ySub = 0; ySub < 4; ++ySub) paulo@0: { paulo@0: for(int xSub = 0; xSub <= 3; ++xSub) paulo@0: { paulo@0: int blkHere = p->b[y + ySub][x + xSub]; paulo@0: paulo@0: /* the square contains no nonexistent blocks */ paulo@0: if(!blkHere) paulo@0: return 0; paulo@0: /* the square contains no blocks of garbage or broken pieces */ paulo@0: if((blkHere & COLOR_MASK) == 0x80) paulo@0: return 0; paulo@0: /* if looking for monosquares, disallow multisquares */ paulo@0: if(isMulti == 0 && (blkHere & COLOR_MASK) != firstColor) paulo@0: return 0; paulo@0: } paulo@0: } paulo@0: return 1; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Replaces the 4x4 blocks with a 4x4 square. paulo@0: * @param x column of left side, such that 0 <= x <= playfield width - 4 paulo@0: * @param y row of bottom block, such that 0 <= y <= playfield height - 4 paulo@0: * @param isMulti nonzero for multisquares; 0 for monosquares paulo@0: * @return the rows that were changed paulo@0: */ paulo@0: static LJBits markSquare(LJField *p, int x, int y, int isMulti) paulo@0: { paulo@0: int baseBlk = (isMulti ? 0xA0 : 0xB0) paulo@0: | CONNECT_MASK; paulo@0: for(int i = 0; i < 4; ++i) paulo@0: { paulo@0: int c; paulo@0: paulo@0: if(i == 0) paulo@0: c = baseBlk & ~CONNECT_D; paulo@0: else if(i == 3) paulo@0: c = baseBlk & ~CONNECT_U; paulo@0: else paulo@0: c = baseBlk; paulo@0: paulo@0: p->b[y + i][x + 0] = c & ~CONNECT_L; paulo@0: p->b[y + i][x + 1] = c; paulo@0: p->b[y + i][x + 2] = c; paulo@0: p->b[y + i][x + 3] = c & ~CONNECT_R; paulo@0: } paulo@0: paulo@0: if (isMulti) { paulo@0: ++p->multisquares; paulo@0: } else { paulo@0: ++p->monosquares; paulo@0: } paulo@0: paulo@0: return 0x0F << y; paulo@0: } paulo@0: paulo@0: paulo@0: /** paulo@0: * Marks all 4x4 squares in the playfield. paulo@0: * In the case that a single tetromino forms multiple overlapping paulo@0: * squares, prefers gold over silver, high over low, left over right. paulo@0: * @param isMulti nonzero for multisquares; 0 for monosquares paulo@0: * @return the rows that were changed paulo@0: */ paulo@0: static LJBits findSquares(LJField *p, int isMulti) { paulo@0: LJBits changed = 0; paulo@0: paulo@0: for (int y = LJ_PF_HT - 4; y >= 0; --y) { paulo@0: for (int x = p->leftWall; x <= p->rightWall - 4; ++x) { paulo@0: int baseBlk = p->b[y][x]; paulo@0: paulo@0: // If this block is filled in and not connected on the left or right paulo@0: // then do stuff to it paulo@0: if (baseBlk paulo@0: && !(baseBlk & (CONNECT_D | CONNECT_L)) paulo@0: && isSquareAt(p, x, y, isMulti)) { paulo@0: changed |= markSquare(p, x, y, isMulti); paulo@0: p->sounds |= LJSND_SQUARE; paulo@0: } paulo@0: } paulo@0: } paulo@0: return changed; paulo@0: } paulo@0: paulo@0: static LJBits stickyGluing(LJField *p) { paulo@0: LJBits changed = 0; paulo@0: int byColor = (p->gluing == LJGLUING_STICKY_BY_COLOR); paulo@0: paulo@0: for (int y = 0; y < LJ_PF_HT; ++y) { paulo@0: for (int x = p->leftWall; x < p->rightWall - 1; ++x) { paulo@0: int l = p->b[y][x]; paulo@0: int r = p->b[y][x + 1]; paulo@0: if (l && r paulo@0: && (!(l & CONNECT_R) || !(r & CONNECT_L)) paulo@0: && (!byColor || !((l ^ r) & COLOR_MASK))) { paulo@0: p->b[y][x] = l | CONNECT_R; paulo@0: p->b[y][x + 1] = r | CONNECT_L; paulo@0: changed |= 1 << y; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: for (int y = 0; y < LJ_PF_HT - 1; ++y) { paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: int b = p->b[y][x]; paulo@0: int t = p->b[y + 1][x]; paulo@0: if (b && t paulo@0: && (!(b & CONNECT_U) || !(t & CONNECT_D)) paulo@0: && (!byColor || !((b ^ t) & COLOR_MASK))) { paulo@0: p->b[y][x] = b | CONNECT_U; paulo@0: p->b[y + 1][x] = t | CONNECT_D; paulo@0: changed |= 3 << y; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: return changed; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Locks the current tetromino in the playfield. paulo@0: * @param p the playfield paulo@0: * @return the rows in which the tetromino was placed paulo@0: */ paulo@0: static LJBits lockPiece(LJField *p) { paulo@0: LJBits rows = 0; paulo@0: int xBase = p->x; paulo@0: int yBase = ljfixfloor(p->y); paulo@0: LJBlkSpec blocks[4]; paulo@0: int piece = p->curPiece[0]; paulo@0: expandPieceToBlocks(blocks, p, piece, xBase, yBase, p->theta); paulo@0: paulo@0: p->isSpin = isTspin(p); paulo@0: paulo@0: for (int blk = 0; blk < 4; ++blk) { paulo@0: int blkY = blocks[blk].y; paulo@0: int blkX = blocks[blk].x; paulo@0: int blkValue = blocks[blk].conn; paulo@0: paulo@0: if(blkValue && blkY >= 0 && blkY < LJ_PF_HT) { paulo@0: rows |= 1 << blkY; paulo@0: if (blkX >= p->leftWall && blkX < p->rightWall) { paulo@0: p->b[blkY][blkX] = blkValue; paulo@0: } paulo@0: } paulo@0: } paulo@0: p->sounds |= LJSND_LOCK; paulo@0: p->alreadyHeld = 0; paulo@0: p->nLinesThisPiece = 0; paulo@0: paulo@0: return rows; paulo@0: } paulo@0: paulo@0: void shuffleColumns(LJField *p) { paulo@0: unsigned int permu[LJ_PF_WID]; paulo@0: paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: permu[x] = x; paulo@0: } paulo@0: for (int x = p->rightWall - 1; x > p->rightWall + 1; --x) { paulo@0: int r = ljRand(p) % x; paulo@0: int t = permu[x]; paulo@0: permu[x] = permu[r]; paulo@0: permu[r] = t; paulo@0: } paulo@0: paulo@0: for (int y = 0; y < LJ_PF_HT; ++y) { paulo@0: unsigned int blk[LJ_PF_WID]; paulo@0: paulo@0: // Copy blocks to temporary buffer, eliminating left and right connections paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: blk[x] = p->b[y][permu[x]] & ~(CONNECT_L | CONNECT_R); paulo@0: } paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: p->b[y][x] = blk[x]; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: /** paulo@0: * Rotates and shifts a new piece per the rotation system. paulo@0: */ paulo@0: static void rotateNewPiece(LJField *p) { paulo@0: const LJRotSystem *rs = rotSystems[p->rotationSystem]; paulo@0: int shape = p->curPiece[0] & LJP_MASK; paulo@0: int kickData = rs->entryOffset[shape]; paulo@0: p->x += WKX(kickData); paulo@0: p->y += ljitofix(WKY(kickData)); paulo@0: p->theta = rs->entryTheta[shape]; paulo@0: paulo@0: /* Find extents of piece so that it doesn't enter already collided paulo@0: with the walls (otherwise, in Tengen rotation and well width 4, paulo@0: spawning I causes block out) */ paulo@0: paulo@0: LJBlkSpec blocks[4]; paulo@0: int minX = LJ_PF_WID - 1, maxX = 0, maxY = 0; paulo@0: paulo@0: expandPieceToBlocks(blocks, p, p->curPiece[0], paulo@0: p->x, ljfixfloor(p->y), p->theta); paulo@0: for (int blk = 0; blk < 4; ++blk) { paulo@0: if (blocks[blk].conn) { paulo@0: if (maxY < blocks[blk].y) { paulo@0: maxY = blocks[blk].y; paulo@0: } paulo@0: if (maxX < blocks[blk].x) { paulo@0: maxX = blocks[blk].x; paulo@0: } paulo@0: if (minX > blocks[blk].x) { paulo@0: minX = blocks[blk].x; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: if (minX < p->leftWall) { paulo@0: p->x += minX - p->leftWall; paulo@0: } else if (maxX >= p->rightWall) { paulo@0: p->x -= (maxX + 1) - p->rightWall; paulo@0: } paulo@0: if (!p->enterAbove && maxY >= p->ceiling) { paulo@0: p->y -= ljitofix(maxY - p->ceiling) + 1; paulo@0: } paulo@0: } paulo@0: paulo@0: paulo@0: /** paulo@0: * Spawns a tetromino onto the playfield. paulo@0: * @param p the playfield paulo@0: * @param hold 0 for spawning from next; nonzero for swapping with hold paulo@0: * @return the rows that were changed by banana effect paulo@0: */ paulo@0: static LJBits newPiece(LJField *p, int hold) { paulo@0: LJBits changed = 0; paulo@0: int initial = p->state != LJS_FALLING paulo@0: && p->state != LJS_LANDED; paulo@0: int ihs = initial && hold; paulo@0: paulo@0: if (hold) { paulo@0: if (p->state == LJS_LANDED && p->lockReset == LJLOCK_SPAWN) { paulo@0: p->speed.lockDelay = p->stateTime; paulo@0: } paulo@0: } else { paulo@0: p->upwardKicks = 0; paulo@0: } paulo@0: p->x = (LJ_PF_WID - 4) / 2; paulo@0: p->dropDist = 0; paulo@0: if (!ihs) { paulo@0: p->state = LJS_FALLING; paulo@0: p->stateTime = 0; paulo@0: } paulo@0: p->isSpin = 0; paulo@0: p->y = ljitofix(p->ceiling - 2); paulo@0: paulo@0: /* Note: The gimmick sets the gravity speed after frame() finishes. */ paulo@0: paulo@0: if (hold) { paulo@0: int temp; paulo@0: paulo@0: if (p->holdStyle != LJHOLD_TO_NEXT) { paulo@0: temp = p->holdPiece; paulo@0: p->holdPiece = p->curPiece[ihs]; paulo@0: } else { paulo@0: temp = p->curPiece[ihs + 1]; paulo@0: p->curPiece[ihs + 1] = p->curPiece[ihs]; paulo@0: } paulo@0: p->curPiece[ihs] = temp; paulo@0: p->alreadyHeld = 1; paulo@0: p->sounds |= LJSND_HOLD; paulo@0: paulo@0: // If a negative number was swapped into the current piece, paulo@0: // then there was nothing in the hold space (e.g. at the paulo@0: // beginning of a round). In this case, we'll need to fall paulo@0: // through and generate a new piece. paulo@0: if (temp >= 0) { paulo@0: rotateNewPiece(p); paulo@0: return changed; paulo@0: } paulo@0: } paulo@0: paulo@0: // Shift the next pieces down paulo@0: for (int i = 0; i < LJ_NEXT_PIECES; i++) { paulo@0: p->curPiece[i] = p->curPiece[i + 1]; paulo@0: } paulo@0: p->sounds |= LJSND_SPAWN; paulo@0: paulo@0: p->curPiece[LJ_NEXT_PIECES] = randomize(p); paulo@0: ++p->nPieces; paulo@0: if (!p->canRotate) { paulo@0: p->theta = (ljRand(p) >> 12) & 0x03; paulo@0: } else { paulo@0: rotateNewPiece(p); paulo@0: } paulo@0: paulo@0: return changed; paulo@0: } paulo@0: paulo@0: void newGame(LJField *p) { paulo@0: paulo@0: // Clear playfield paulo@0: for (int y = 0; y < LJ_PF_HT; y++) { paulo@0: for(int x = 0; x < LJ_PF_WID; x++) { paulo@0: p->b[y][x] = 0; paulo@0: } paulo@0: } paulo@0: paulo@0: for (int y = 0; y < LJ_MAX_LINES_PER_PIECE; ++y) { paulo@0: p->nLineClears[y] = 0; paulo@0: } paulo@0: paulo@0: if (p->holdStyle == LJHOLD_TNT) { paulo@0: initRandomize(p); paulo@0: p->holdPiece = randomize(p); paulo@0: } else { paulo@0: p->holdPiece = -1; // sentinel for no piece in hold box paulo@0: } paulo@0: paulo@0: // Generate pieces paulo@0: initRandomize(p); paulo@0: for(int i = 0; i < LJ_NEXT_PIECES; i++) { paulo@0: newPiece(p, 0); paulo@0: } paulo@0: p->clearedLines = 0; paulo@0: p->nPieces = 0; paulo@0: p->state = LJS_NEW_PIECE; paulo@0: p->stateTime = 1; paulo@0: p->garbage = 0; paulo@0: p->outGarbage = 0; paulo@0: p->score = 0; paulo@0: p->gameTime = 0; paulo@0: p->activeTime = 0; paulo@0: p->lines = 0; paulo@0: p->alreadyHeld = 0; paulo@0: p->chain = 0; paulo@0: p->theta = 0; paulo@0: p->nLinesThisPiece = 0; paulo@0: p->canRotate = 1; paulo@0: p->speed.entryDelay = 0; paulo@0: p->garbageRandomness = 64; paulo@0: p->reloaded = 0; paulo@0: p->monosquares = 0; paulo@0: p->multisquares = 0; paulo@0: paulo@0: p->garbageX = ljRand(p) % (p->rightWall - p->leftWall) paulo@0: + p->leftWall; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Handles scoring for hard and soft drops. paulo@0: * @return 0 for no change or LJ_DIRTY_SCORE for change paulo@0: */ paulo@0: LJBits scoreDropRows(LJField *p, LJFixed gravity, LJFixed newY) { paulo@0: LJBits changed = 0; paulo@0: if (gravity > 0) { paulo@0: int fallDist = ljfixfloor(p->y) - ljfixfloor(newY); paulo@0: paulo@0: p->dropDist += fallDist; paulo@0: paulo@0: // Double scoring for hard drop paulo@0: if (p->dropScoreStyle == LJDROP_1S_2H paulo@0: && gravity >= ljitofix(p->ceiling)) { paulo@0: fallDist *= 2; paulo@0: } paulo@0: if (p->dropScoreStyle == LJDROP_1CELL paulo@0: || p->dropScoreStyle == LJDROP_1S_2H) { paulo@0: p->score += fallDist; paulo@0: changed |= LJ_DIRTY_SCORE; paulo@0: } paulo@0: paulo@0: // Handle scoring for continuous drop paulo@0: if (p->dropScoreStyle == LJDROP_NES paulo@0: && newY <= ljitofix(p->hardDropY)) { paulo@0: p->score += p->dropDist; paulo@0: changed |= LJ_DIRTY_SCORE; paulo@0: p->dropDist = 0; paulo@0: } paulo@0: } else { paulo@0: p->dropDist = 0; paulo@0: } paulo@0: return changed; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Handles gravity. paulo@0: * @param p the playfield paulo@0: * @param gravity amount of additional gravity applied by the player paulo@0: * @param otherKeys other LJI_* keys being pressed by the player paulo@0: * (specifically LJI_LOCK) paulo@0: */ paulo@0: static LJBits doPieceGravity(LJField *p, LJFixed gravity, LJBits otherKeys) { paulo@0: int changedRows = 0; paulo@0: paulo@0: LJFixed newY = p->y - gravity - p->speed.gravity; paulo@0: paulo@0: // Check for landed paulo@0: if (newY <= ljitofix(p->hardDropY)) { paulo@0: newY = ljitofix(p->hardDropY); paulo@0: paulo@0: // Downward movement does not result in a T-spin paulo@0: if (ljfixfloor(newY) < ljfixfloor(p->y)) { paulo@0: p->isSpin = 0; paulo@0: } paulo@0: paulo@0: changedRows |= scoreDropRows(p, gravity, newY); paulo@0: p->y = newY; paulo@0: paulo@0: if (p->state == LJS_FALLING) { paulo@0: p->state = LJS_LANDED; paulo@0: p->stateTime = p->speed.lockDelay; paulo@0: p->sounds |= LJSND_LAND; paulo@0: } paulo@0: if (p->stateTime > 0 && !(otherKeys & LJI_LOCK)) { paulo@0: // lock delay > 128 is a special case for don't lock at all paulo@0: if (p->setLockDelay < 128) { paulo@0: --p->stateTime; paulo@0: } paulo@0: } else { paulo@0: LJBits lockRows = lockPiece(p); paulo@0: p->state = LJS_LINES; paulo@0: p->stateTime = 0; paulo@0: changedRows |= lockRows | LJ_DIRTY_NEXT; paulo@0: paulo@0: // LOCK OUT rule: If a piece locks paulo@0: // completely above the ceiling, the game is over. paulo@0: if (!(lockRows & ((1 << p->ceiling) - 1))) { paulo@0: p->state = LJS_GAMEOVER; paulo@0: } paulo@0: } paulo@0: } else { paulo@0: changedRows |= scoreDropRows(p, gravity, newY); paulo@0: p->state = LJS_FALLING; paulo@0: paulo@0: // Downward movement does not result in a T-spin paulo@0: if (ljfixfloor(newY) < ljfixfloor(p->y)) { paulo@0: p->isSpin = 0; paulo@0: } paulo@0: } paulo@0: p->y = newY; paulo@0: return changedRows; paulo@0: } paulo@0: paulo@0: static void updateLockDelayOnMove(LJField *p, int isUpwardKick) { paulo@0: if (p->state == LJS_LANDED) { paulo@0: paulo@0: // if tetromino can move down, go back to falling state; paulo@0: // otherwise, reset lock delay. paulo@0: if (!isCollision(p, p->x, ljfixfloor(p->y) - 1, p->theta)) { paulo@0: p->state = LJS_FALLING; paulo@0: if (p->lockReset == LJLOCK_SPAWN) { paulo@0: p->speed.lockDelay = p->stateTime; paulo@0: } paulo@0: p->stateTime = 0; paulo@0: } else { paulo@0: p->state = LJS_LANDED; paulo@0: if (p->lockReset == LJLOCK_MOVE paulo@0: || (p->lockReset == LJLOCK_STEP && isUpwardKick)) { paulo@0: p->stateTime = p->speed.lockDelay; paulo@0: } paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: static int doRotate(LJField *p, paulo@0: int newDir, paulo@0: const WallKickTable *const pieceTable, paulo@0: int withKicks) { paulo@0: int baseX = p->x; paulo@0: int baseY = ljfixfloor(p->y); paulo@0: paulo@0: withKicks = withKicks ? KICK_TABLE_LEN : 1; paulo@0: paulo@0: // allow specifying null tables for O paulo@0: if (!pieceTable) { paulo@0: if (!isCollision(p, baseX, baseY, newDir)) { paulo@0: p->theta = newDir; paulo@0: p->sounds |= LJSND_ROTATE; paulo@0: return 1; paulo@0: } paulo@0: return 0; paulo@0: } paulo@0: paulo@0: const unsigned char *const table = (*pieceTable)[newDir]; paulo@0: int baseKickY = -1000; // sentinel for uninitialized paulo@0: paulo@0: for (int kick = 0; kick < withKicks; kick++) { paulo@0: unsigned int kickData = table[kick]; paulo@0: if (kickData == WK_END) { paulo@0: break; paulo@0: } else if (kickData == ARIKA_IF_NOT_CENTER) { paulo@0: paulo@0: // Compute the free space position paulo@0: kickData = table[0]; paulo@0: int kickX = WKX(kickData) + baseX; paulo@0: int kickY = WKY(kickData) + baseY; paulo@0: LJBlkSpec blocks[4]; paulo@0: int allowed = 0; paulo@0: paulo@0: // If a block other than the center column of this position paulo@0: // is occupied, go to the next step (that is, paulo@0: // allow subsequent kicks) paulo@0: expandPieceToBlocks(blocks, p, p->curPiece[0], paulo@0: kickX, kickY, newDir); paulo@0: paulo@0: for (int blk = 0; blk < 4; ++blk) { paulo@0: if (blocks[blk].conn paulo@0: && blocks[blk].x != baseX + 1 paulo@0: && isOccupied(p, blocks[blk].x, blocks[blk].y)) { paulo@0: allowed = 1; paulo@0: break; paulo@0: } paulo@0: } paulo@0: paulo@0: // Otherwise, only blocks of the center column are occupied, paulo@0: // and these cannot kick the piece. paulo@0: if (!allowed) { paulo@0: return 0; paulo@0: } paulo@0: } else { paulo@0: int kickX = WKX(kickData) + baseX; paulo@0: int kickY = WKY(kickData) + baseY; paulo@0: paulo@0: // If this is the first paulo@0: if (baseKickY == -1000) { paulo@0: baseKickY = kickY; paulo@0: } paulo@0: if ((kickY <= baseKickY || p->upwardKicks < p->maxUpwardKicks) paulo@0: && !isCollision(p, kickX, kickY, newDir)) { paulo@0: p->theta = newDir; paulo@0: p->x = kickX; paulo@0: if (kickY > baseKickY) { paulo@0: p->y = ljitofix(kickY); paulo@0: paulo@0: // on the FIRST floor kick of a piece, reset lock delay paulo@0: if (p->upwardKicks == 0) { paulo@0: updateLockDelayOnMove(p, 1); paulo@0: } paulo@0: ++p->upwardKicks; paulo@0: } else if (kickY < baseKickY) { paulo@0: p->y = ljitofix(kickY) + 0xFFFF; paulo@0: } else { paulo@0: p->y = ljitofix(kickY) + (p->y & 0xFFFF); paulo@0: } paulo@0: p->sounds |= LJSND_ROTATE; paulo@0: return 1; paulo@0: } paulo@0: } paulo@0: } paulo@0: return 0; paulo@0: } paulo@0: paulo@0: paulo@0: /** paulo@0: * Tries to rotate the current piece 90 degrees counterclockwise, paulo@0: * using the counterclockwise wall kick tables. paulo@0: * @param p the playfield paulo@0: * @param withKicks nonzero for wall kick, zero for none paulo@0: */ paulo@0: static int doRotateLeft(LJField *p, int withKicks) { paulo@0: int newDir = (p->theta + 3) & 0x03; paulo@0: const LJRotSystem *rs = rotSystems[p->rotationSystem]; paulo@0: signed int tableNo = rs->kicksL[p->curPiece[0] & LJP_MASK]; paulo@0: const WallKickTable *pieceTable = tableNo >= 0 paulo@0: ? &(rs->kickTables[tableNo]) paulo@0: : NULL; paulo@0: return doRotate(p, newDir, pieceTable, withKicks); paulo@0: } paulo@0: paulo@0: /** paulo@0: * Tries to rotate the current piece 90 degrees clockwise, paulo@0: * using the clockwise wall kick tables. paulo@0: * @param p the playfield paulo@0: * @param withKicks nonzero for wall kick, zero for none paulo@0: */ paulo@0: static int doRotateRight(LJField *p, int withKicks) { paulo@0: int newDir = (p->theta + 1) & 0x03; paulo@0: const LJRotSystem *rs = rotSystems[p->rotationSystem]; paulo@0: signed int tableNo = rs->kicksR[p->curPiece[0] & LJP_MASK]; paulo@0: const WallKickTable *pieceTable = tableNo >= 0 paulo@0: ? &(rs->kickTables[tableNo]) paulo@0: : NULL; paulo@0: return doRotate(p, newDir, pieceTable, withKicks); paulo@0: } paulo@0: paulo@0: static LJBits checkLines(const LJField *p, LJBits checkRows) { paulo@0: LJBits foundLines = 0; paulo@0: for (int y = 0; paulo@0: y < LJ_PF_HT && checkRows != 0; paulo@0: ++y, checkRows >>= 1) { paulo@0: if (checkRows & 1) { paulo@0: const unsigned char *row = p->b[y]; paulo@0: int found = 1; paulo@0: paulo@0: for (int x = p->leftWall; x < p->rightWall && found; ++x) { paulo@0: found = found && (row[x] != 0); paulo@0: } paulo@0: if (found) { paulo@0: foundLines |= 1 << y; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: return foundLines; paulo@0: } paulo@0: paulo@0: static void fillCLoop(LJField *p, int x, int y, unsigned int src, unsigned int dst) paulo@0: { paulo@0: int fillL, fillR, i; paulo@0: paulo@0: fillL = fillR = x; paulo@0: do { paulo@0: p->c[y][fillL] = dst; paulo@0: fillL--; paulo@0: } while ((fillL >= p->leftWall) && (p->c[y][fillL] == src)); paulo@0: fillL++; paulo@0: paulo@0: do { paulo@0: p->c[y][fillR] = dst; paulo@0: fillR++; paulo@0: } while ((fillR < p->rightWall) && (p->c[y][fillR] == src)); paulo@0: fillR--; paulo@0: paulo@0: for(i = fillL; i <= fillR; i++) paulo@0: { paulo@0: if(y > 0 && p->c[y - 1][i] == src) { paulo@0: fillCLoop(p, i, y - 1, src, dst); paulo@0: } paulo@0: if(y < LJ_PF_HT - 1 && p->c[y + 1][i] == src) { paulo@0: fillCLoop(p, i, y + 1, src, dst); paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: paulo@0: static void fillC(LJField *p, int x, int y, int dstC) { paulo@0: if (p->c[y][x] != dstC) { paulo@0: fillCLoop(p, x, y, p->c[y][x], dstC); paulo@0: } paulo@0: } paulo@0: paulo@0: /** paulo@0: * Locks the block regions that have landed. paulo@0: * @param p the playfield paulo@0: */ paulo@0: void lockLandedRegions(LJField *p) { paulo@0: // Look for regions that are on top of ground regions, where paulo@0: // "ground regions" are any block that is solid and whose region ID is 0. paulo@0: for (int landed = 1; landed != 0; ) { paulo@0: landed = 0; paulo@0: // If something hit the ground, erase its floating bit paulo@0: for (int y = 0; y < LJ_PF_HT; ++y) { paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: // If there's a floating block here, and a not-floating block below, paulo@0: // erase this block group's floatiness paulo@0: if (p->c[y][x] && paulo@0: (y == 0 || (!p->c[y - 1][x] && p->b[y - 1][x]))) { paulo@0: fillC(p, x, y, 0); paulo@0: p->sounds |= LJSND_LAND; paulo@0: landed = 1; paulo@0: } paulo@0: } paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: /** paulo@0: * Separates the playfield into regions that shall fall separately. paulo@0: * @param p the playfield paulo@0: * @param byColors Zero: Touching blocks form a region. paulo@0: * Nonzero: Touching blocks of a single color form a region. paulo@0: */ paulo@0: static void stickyMark(LJField *p, int byColors) { paulo@0: for (unsigned int y = 0; y < LJ_PF_HT; ++y) { paulo@0: for (unsigned int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: int blkHere = p->b[y][x] & COLOR_MASK; paulo@0: paulo@0: if (!byColors) { paulo@0: blkHere = blkHere ? 0x10 : 0; paulo@0: } paulo@0: p->c[y][x] = blkHere; paulo@0: } paulo@0: } paulo@0: paulo@0: if (byColors) { paulo@0: lockLandedRegions(p); paulo@0: } else { paulo@0: // mark the bottom row as landed paulo@0: for (unsigned int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: if (p->c[0][x]) { paulo@0: fillC(p, x, 0, 0); paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: //p->stateTime = 5; paulo@0: } paulo@0: paulo@0: paulo@0: /** paulo@0: * Sets the color of a piece to gray/garbage (0x80). paulo@0: * @param x column of a block in the piece paulo@0: * @param y row of a block in the piece paulo@0: * @param rgn the region ID paulo@0: */ paulo@0: static void cascadeMarkPiece(LJField *p, int x, int y, int rgn) { paulo@0: int blkHere = p->b[y][x]; paulo@0: paulo@0: if (blkHere && !p->c[y][x]) { paulo@0: p->c[y][x] = rgn; paulo@0: if((blkHere & CONNECT_D) && y > 0) paulo@0: cascadeMarkPiece(p, x, y - 1, rgn); paulo@0: if((blkHere & CONNECT_U) && y < LJ_PF_HT - 1) paulo@0: cascadeMarkPiece(p, x, y + 1, rgn); paulo@0: if((blkHere & CONNECT_L) && x > p->leftWall) paulo@0: cascadeMarkPiece(p, x - 1, y, rgn); paulo@0: if((blkHere & CONNECT_R) && x < p->rightWall - 1 ) paulo@0: cascadeMarkPiece(p, x + 1, y, rgn); paulo@0: } paulo@0: } paulo@0: paulo@0: static void cascadeMark(LJField *p) { paulo@0: int rgn = 0; paulo@0: paulo@0: for (unsigned int y = 0; y < LJ_PF_HT; ++y) { paulo@0: for (unsigned int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: p->c[y][x] = 0; paulo@0: } paulo@0: } paulo@0: for (unsigned int y = 0; y < LJ_PF_HT; ++y) { paulo@0: for (unsigned int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: cascadeMarkPiece(p, x, y, ++rgn); paulo@0: } paulo@0: } paulo@0: lockLandedRegions(p); paulo@0: //p->stateTime = 5; paulo@0: } paulo@0: paulo@0: static void breakEverything(LJField *p) { paulo@0: for (unsigned int y = 0; y < LJ_PF_HT; ++y) { paulo@0: for (unsigned int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: if (p->b[y][x]) { paulo@0: p->c[y][x] = x + 1; paulo@0: p->b[y][x] = 0x80; paulo@0: } else { paulo@0: p->c[y][x] = 0; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: // fill bottom row paulo@0: for (unsigned int x = x = p->leftWall; x < p->rightWall; ++x) { paulo@0: if (p->c[0][x]) { paulo@0: fillC(p, x, 0, 0); paulo@0: } paulo@0: } paulo@0: p->stateTime = 5; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Sets the color of a piece to gray/garbage (0x80). paulo@0: * @param x column of a block in the piece paulo@0: * @param y row of a block in the piece paulo@0: */ paulo@0: static LJBits breakPiece(LJField *p, int x, int y) { paulo@0: LJBits changed = 0; paulo@0: int blkHere = p->b[y][x]; paulo@0: int colorHere = blkHere & COLOR_MASK; paulo@0: int connHere = blkHere & CONNECT_MASK; paulo@0: paulo@0: if (colorHere != 0x80) { paulo@0: p->b[y][x] = connHere | 0x80; paulo@0: changed |= 1 << y; paulo@0: if((blkHere & CONNECT_D) && y > 0) paulo@0: changed |= breakPiece(p, x, y - 1); paulo@0: if((blkHere & CONNECT_U) && y < LJ_PF_HT - 1) paulo@0: changed |= breakPiece(p, x, y + 1); paulo@0: if((blkHere & CONNECT_L) && x > p->leftWall) paulo@0: changed |= breakPiece(p, x - 1, y); paulo@0: if((blkHere & CONNECT_R) && x < p->rightWall - 1 ) paulo@0: changed |= breakPiece(p, x + 1, y); paulo@0: } paulo@0: return changed; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Removes blocks in cleared lines from the playfield and marks paulo@0: * remaining blocks for gravity. paulo@0: * @param the lines to be cleared paulo@0: * @return the rows that were changed paulo@0: */ paulo@0: static LJBits clearLines(LJField *p, LJBits foundLines) { paulo@0: LJBits changed = foundLines; paulo@0: paulo@0: p->clearedLines = foundLines; paulo@0: if (foundLines != 0) { paulo@0: p->sounds |= LJSND_LINE; paulo@0: } paulo@0: for (int y = 0; paulo@0: y < LJ_PF_HT && foundLines != 0; paulo@0: ++y, foundLines >>= 1) { paulo@0: if (foundLines & 1) { paulo@0: paulo@0: // In square mode, turn broken pieces (but not 4x4 squares) paulo@0: // into garbage blocks paulo@0: if (p->gluing == LJGLUING_SQUARE) { paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: if (p->b[y][x] < 0x80) { paulo@0: changed |= breakPiece(p, x, y); paulo@0: } else if ((p->b[y][x] & (0xF0 | CONNECT_R)) == 0xA0) { paulo@0: p->score += 500; paulo@0: changed |= LJ_DIRTY_SCORE; paulo@0: } else if ((p->b[y][x] & (0xF0 | CONNECT_R)) == 0xB0) { paulo@0: p->score += 1000; paulo@0: changed |= LJ_DIRTY_SCORE; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: p->b[y][x] = 0; paulo@0: } paulo@0: paulo@0: // break connections up and down (like Tengen Tetyais) paulo@0: if (y > 0) { paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: p->b[y - 1][x] &= ~CONNECT_U; paulo@0: } paulo@0: changed |= 1 << (y - 1); paulo@0: } paulo@0: if (y < LJ_PF_HT - 1) { paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: p->b[y + 1][x] &= ~CONNECT_D; paulo@0: } paulo@0: changed |= 1 << (y + 1); paulo@0: } paulo@0: } paulo@0: } paulo@0: if (p->gluing == LJGLUING_SQUARE && p->isSpin) { paulo@0: breakEverything(p); paulo@0: changed |= (1 << LJ_PF_HT) - 1; paulo@0: } else if (p->clearGravity == LJGRAV_STICKY) { paulo@0: stickyMark(p, 0); paulo@0: } else if (p->clearGravity == LJGRAV_STICKY_BY_COLOR) { paulo@0: stickyMark(p, 1); paulo@0: } else if (p->clearGravity == LJGRAV_CASCADE) { paulo@0: cascadeMark(p); paulo@0: } else { paulo@0: p->stateTime = 0; paulo@0: } paulo@0: paulo@0: return changed; paulo@0: } paulo@0: paulo@0: static unsigned int stickyFallLines(LJField *p) { paulo@0: int minY = LJ_PF_HT; paulo@0: paulo@0: // Move floating stuff down by one block paulo@0: for (int y = 1; y < LJ_PF_HT; ++y) { paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: int c = p->c[y][x]; paulo@0: if (c) { paulo@0: p->c[y - 1][x] = c; paulo@0: p->c[y][x] = 0; paulo@0: p->b[y - 1][x] = p->b[y][x]; paulo@0: p->b[y][x] = 0; paulo@0: paulo@0: if (minY > y) { paulo@0: minY = y; paulo@0: } paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: // If we're done, skip all the rest paulo@0: if (minY >= LJ_PF_HT) { paulo@0: return LJ_PF_HT; paulo@0: } paulo@0: paulo@0: lockLandedRegions(p); paulo@0: return minY - 1; paulo@0: } paulo@0: paulo@0: paulo@0: unsigned int bfffo(LJBits rowBits) { paulo@0: unsigned int lineRow = 0; paulo@0: paulo@0: if (!rowBits) { paulo@0: return 32; paulo@0: } paulo@0: if ((rowBits & 0xFFFF) == 0) { paulo@0: rowBits >>= 16; paulo@0: lineRow += 16; paulo@0: } paulo@0: if ((rowBits & 0xFF) == 0) { paulo@0: rowBits >>= 8; paulo@0: lineRow += 8; paulo@0: } paulo@0: if ((rowBits & 0xF) == 0) { paulo@0: rowBits >>= 4; paulo@0: lineRow += 4; paulo@0: } paulo@0: if ((rowBits & 0x3) == 0) { paulo@0: rowBits >>= 2; paulo@0: lineRow += 2; paulo@0: } paulo@0: if ((rowBits & 0x1) == 0) { paulo@0: rowBits >>= 1; paulo@0: lineRow += 1; paulo@0: } paulo@0: return lineRow; paulo@0: } paulo@0: paulo@0: static unsigned int fallLines(LJField *p) { paulo@0: LJBits rowBits = p->tempRows; paulo@0: unsigned int lineRow = 0; paulo@0: paulo@0: if (p->clearGravity != LJGRAV_NAIVE paulo@0: || (p->gluing == LJGLUING_SQUARE && p->isSpin)) { paulo@0: return stickyFallLines(p); paulo@0: } paulo@0: paulo@0: if (rowBits == 0) { paulo@0: return LJ_PF_HT; paulo@0: } paulo@0: paulo@0: lineRow = bfffo(rowBits); paulo@0: p->tempRows = (p->tempRows & (-2 << lineRow)) >> 1; paulo@0: if (!(p->tempRows & (1 << lineRow))) { paulo@0: p->sounds |= LJSND_LAND; paulo@0: } paulo@0: paulo@0: // Move stuff down by 1 row paulo@0: for (int y = lineRow; y < LJ_PF_HT - 1; ++y) { paulo@0: unsigned char *row0 = p->b[y]; paulo@0: const unsigned char *row1 = p->b[y + 1]; paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: row0[x] = row1[x]; paulo@0: } paulo@0: } paulo@0: paulo@0: // Clear top row paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: p->b[LJ_PF_HT - 1][x] = 0; paulo@0: } paulo@0: paulo@0: return lineRow; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Counts the bits in a bit vector that are true (1). paulo@0: * @param b a bit vector paulo@0: * @return the number of 1 bits paulo@0: */ paulo@0: unsigned int countOnes(LJBits b) { paulo@0: unsigned int ones = 0; paulo@0: paulo@0: while(b) { paulo@0: ++ones; paulo@0: b &= b - 1; paulo@0: } paulo@0: return ones; paulo@0: } paulo@0: paulo@0: static unsigned int addGarbage(LJField *p) { paulo@0: // Move stuff up by 1 row paulo@0: for (int y = LJ_PF_HT - 2; y >= 0; --y) { paulo@0: unsigned char *row1 = p->b[y + 1]; paulo@0: const unsigned char *row0 = p->b[y]; paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: row1[x] = row0[x]; paulo@0: } paulo@0: } paulo@0: paulo@0: // Garbage in bottom row paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: p->b[0][x] = 0x80; paulo@0: } paulo@0: paulo@0: // Randomize location of garbage hole paulo@0: int r = (ljRand(p) >> 7) & 0xFF; paulo@0: int garbageX = (r <= p->garbageRandomness) paulo@0: ? (ljRand(p) % (p->rightWall - p->leftWall)) + p->leftWall paulo@0: : p->garbageX; paulo@0: p->b[0][garbageX] = 0; paulo@0: p->garbageX = garbageX; paulo@0: paulo@0: // Horizontally connect the blocks that make up garbage in bottom row paulo@0: for (int x = p->leftWall; x < p->rightWall - 1; ++x) { paulo@0: if (p->b[0][x] && p->b[0][x + 1]) { paulo@0: p->b[0][x] |= CONNECT_R; paulo@0: p->b[0][x + 1] |= CONNECT_L; paulo@0: } paulo@0: } paulo@0: paulo@0: // Vertically connect the blocks that make up garbage in bottom row paulo@0: for (int x = p->leftWall; x < p->rightWall; ++x) { paulo@0: if (p->b[0][x] paulo@0: && ((p->b[1][x] & COLOR_MASK) == 0x80)) { paulo@0: p->b[0][x] |= CONNECT_U; paulo@0: p->b[1][x] |= CONNECT_D; paulo@0: } paulo@0: } paulo@0: paulo@0: return (1 << LJ_PF_VIS_HT) - 1; paulo@0: } paulo@0: paulo@0: /** paulo@0: * Computes the score and outgoing garbage for lines paulo@0: * and adds them to the player's total. paulo@0: * @param p The playfield paulo@0: * @param lines Bit array where 1 means a line clear on this row. paulo@0: */ paulo@0: void addLinesScore(LJField *p, LJBits lines); paulo@0: paulo@0: /** paulo@0: * Things to do just before launching a new piece. paulo@0: */ paulo@0: void prepareForNewPiece(LJField *p) { paulo@0: int nLines = p->nLinesThisPiece; paulo@0: paulo@0: // Add to number of clears of each number of lines. paulo@0: int idx; paulo@0: paulo@0: if (p->clearGravity == LJGRAV_NAIVE) { paulo@0: // In naive gravity, T-spin single, double, and triple counts paulo@0: // are stored in 5-row, 6-row, and 7-row slots, and T-spins paulo@0: // that clear 0 lines are not counted. paulo@0: idx = (nLines > 4) paulo@0: ? 4 paulo@0: : nLines; paulo@0: paulo@0: if (nLines >= 1 && p->isSpin) { paulo@0: idx += 4; paulo@0: } paulo@0: } else { paulo@0: idx = (nLines > LJ_MAX_LINES_PER_PIECE) paulo@0: ? LJ_MAX_LINES_PER_PIECE paulo@0: : nLines; paulo@0: } paulo@0: paulo@0: if (nLines < 4 && !p->isSpin && p->garbageStyle == LJGARBAGE_HRDERBY) { paulo@0: p->garbage += nLines; paulo@0: } paulo@0: paulo@0: ljAssert(p, paulo@0: idx <= LJ_MAX_LINES_PER_PIECE, paulo@0: "Number of lines cleared with last piece out of bounds in prepareForNewPiece"); paulo@0: if (idx > 0) { paulo@0: p->nLineClears[idx - 1] += 1; paulo@0: } paulo@0: paulo@0: p->state = LJS_NEW_PIECE; paulo@0: p->stateTime = p->speed.entryDelay; paulo@0: } paulo@0: paulo@0: LJBits frame(LJField *p, const LJInput *in) { paulo@0: LJBits changedRows = 0; paulo@0: LJBits tempRows; paulo@0: int distance; paulo@0: int moved = 0; paulo@0: int isFirstFrame = (p->sounds & (LJSND_SPAWN | LJSND_HOLD)) paulo@0: ? 1 : 0; paulo@0: paulo@0: p->sounds = 0; paulo@0: paulo@0: // Make hold work at ANY time. paulo@0: if ((in->other & LJI_HOLD) paulo@0: && p->holdStyle != LJHOLD_NONE paulo@0: && !p->alreadyHeld) { paulo@0: changedRows |= newPiece(p, 1) | LJ_DIRTY_NEXT; paulo@0: updHardDropY(p); paulo@0: } paulo@0: paulo@0: switch(p->state) { paulo@0: case LJS_NEW_PIECE: paulo@0: if (p->garbage > 0) { paulo@0: changedRows |= addGarbage(p); paulo@0: --p->garbage; paulo@0: break; paulo@0: } paulo@0: paulo@0: // ARE paulo@0: if (p->stateTime > 0) { paulo@0: --p->stateTime; paulo@0: } paulo@0: if (p->stateTime > 0) { paulo@0: break; paulo@0: } paulo@0: paulo@0: changedRows |= newPiece(p, 0); paulo@0: updHardDropY(p); paulo@0: changedRows |= LJ_DIRTY_NEXT; paulo@0: paulo@0: /* If the piece spawns over blocks, this is a "block out" and a paulo@0: loss under most rules. But skip checking it now so that paulo@0: the player can IRS out of block out. */ paulo@0: paulo@0: break; paulo@0: paulo@0: // the following executes for both falling and landed paulo@0: case LJS_FALLING: paulo@0: case LJS_LANDED: paulo@0: ++p->activeTime; paulo@0: if (p->canRotate) { paulo@0: int oldX = p->x; paulo@0: int oldY = ljfixfloor(p->y); paulo@0: distance = in->rotation; paulo@0: for(; distance < 0; ++distance) { paulo@0: paulo@0: // 0.43: Do not apply wall kicks on the first frame (IRS) paulo@0: if (doRotateLeft(p, !isFirstFrame)) { paulo@0: moved = 1; paulo@0: paulo@0: // isSpin == 1: twist in place paulo@0: // isSpin == 2: twist with kick paulo@0: // if (p->tSpinAlgo == LJTS_TDS_NO_KICK) paulo@0: // then only isSpin == 1 is worth points. paulo@0: if (p->x == oldX && ljfixfloor(p->y) == oldY) { paulo@0: p->isSpin = 1; paulo@0: } else { paulo@0: p->isSpin = 2; paulo@0: } paulo@0: } else { paulo@0: break; paulo@0: } paulo@0: } paulo@0: for(; distance > 0; --distance) { paulo@0: if (doRotateRight(p, !isFirstFrame)) { paulo@0: moved = 1; paulo@0: if (p->x == oldX && ljfixfloor(p->y) == oldY) { paulo@0: p->isSpin = 1; paulo@0: } else { paulo@0: p->isSpin = 2; paulo@0: } paulo@0: } else { paulo@0: break; paulo@0: } paulo@0: } paulo@0: } paulo@0: paulo@0: /* If the piece spawns over blocks, this is a "block out" and a paulo@0: loss under most rules. Check it now, after rotation, so that paulo@0: the player can IRS out of block out. */ paulo@0: if (isFirstFrame paulo@0: && isCollision(p, p->x, ljfixfloor(p->y), p->theta)) { paulo@0: changedRows |= lockPiece(p); paulo@0: p->state = LJS_GAMEOVER; paulo@0: } paulo@0: paulo@0: distance = in->movement; paulo@0: for(; distance < 0; ++distance) { paulo@0: if (!isCollision(p, p->x - 1, ljfixfloor(p->y), p->theta)) { paulo@0: --p->x; paulo@0: p->sounds |= LJSND_SHIFT; paulo@0: moved = 1; paulo@0: p->isSpin = 0; paulo@0: } paulo@0: } paulo@0: for(; distance > 0; --distance) { paulo@0: if (!isCollision(p, p->x + 1, ljfixfloor(p->y), p->theta)) { paulo@0: ++p->x; paulo@0: p->sounds |= LJSND_SHIFT; paulo@0: moved = 1; paulo@0: p->isSpin = 0; paulo@0: } paulo@0: } paulo@0: updHardDropY(p); paulo@0: if (p->state != LJS_GAMEOVER) { paulo@0: if (moved) { paulo@0: updateLockDelayOnMove(p, 0); paulo@0: } paulo@0: tempRows = doPieceGravity(p, ljitofix(in->gravity) >> 3, in->other); paulo@0: p->tempRows = tempRows; paulo@0: changedRows |= tempRows; paulo@0: } paulo@0: paulo@0: // At this point, if the piece locked, paulo@0: // p->tempRows holds the rows in which the piece landed. paulo@0: break; paulo@0: paulo@0: case LJS_LINES: paulo@0: if (p->stateTime > 0) { paulo@0: --p->stateTime; paulo@0: } paulo@0: if (p->stateTime > 0) { paulo@0: break; paulo@0: } paulo@0: if (p->gluing == LJGLUING_SQUARE) { paulo@0: LJBits gluedRows = findSquares(p, 0); paulo@0: gluedRows |= findSquares(p, 1); paulo@0: changedRows |= gluedRows; paulo@0: if (gluedRows) { paulo@0: paulo@0: // When a 4x4 block square is formed, a delay paulo@0: // equal to the line delay is added. paulo@0: p->stateTime += p->speed.lineDelay; paulo@0: break; paulo@0: } paulo@0: } else if (p->gluing == LJGLUING_STICKY paulo@0: || p->gluing == LJGLUING_STICKY_BY_COLOR) { paulo@0: changedRows |= stickyGluing(p); paulo@0: } paulo@0: paulo@0: // At this point, p->tempRows holds the rows in which paulo@0: // a line could possibly have been made. paulo@0: tempRows = p->tempRows; paulo@0: tempRows = checkLines(p, tempRows); paulo@0: p->tempRows = tempRows; paulo@0: // At this point, p->tempRows holds the rows in which paulo@0: // a line HAS been made. paulo@0: addLinesScore(p, tempRows); paulo@0: changedRows |= LJ_DIRTY_SCORE; paulo@0: paulo@0: // At this point, p->tempRows holds the rows in which a line paulo@0: // HAS been made. paulo@0: p->clearedLines = tempRows; paulo@0: if (!tempRows) { paulo@0: prepareForNewPiece(p); paulo@0: break; paulo@0: } paulo@0: paulo@0: changedRows |= clearLines(p, tempRows); paulo@0: paulo@0: p->state = LJS_LINES_FALLING; paulo@0: p->stateTime += p->speed.lineDelay; paulo@0: break; paulo@0: paulo@0: case LJS_LINES_FALLING: paulo@0: if (p->stateTime > 0) { paulo@0: --p->stateTime; paulo@0: } paulo@0: if (p->stateTime > 0) { paulo@0: break; paulo@0: } paulo@0: moved = fallLines(p); paulo@0: if (moved >= LJ_PF_HT) { paulo@0: p->state = LJS_LINES; paulo@0: p->tempRows = (1 << LJ_PF_HT) - 1; paulo@0: } paulo@0: changedRows |= (~0 << moved) & ((1 << LJ_PF_VIS_HT) - 1); paulo@0: break; paulo@0: paulo@0: default: paulo@0: break; paulo@0: paulo@0: } paulo@0: paulo@0: ++p->gameTime; paulo@0: return changedRows; paulo@0: } paulo@0: paulo@0: