Fix Tetris 2P preview block synchronization issue

- Prevent duplicate piece shapes when generating next pieces
- Enhanced randomTetromino() with excludeShape parameter
- Update piece placement logic to avoid visual duplicates
- Improve initial state generation for both players
- Add P1 and P2 next piece previews in 2P mode
This commit is contained in:
2026-01-06 21:54:11 +01:00
parent 4a1879354b
commit b42ff297eb
13 changed files with 2679 additions and 18 deletions
+65 -18
View File
@@ -58,10 +58,21 @@ interface PlayerState {
const createBoard = (): Board => Array.from({ length: BOARD_HEIGHT }, () => Array(BOARD_WIDTH).fill(null));
const randomTetromino = (isP2 = false): Piece => {
const randomTetromino = (isP2 = false, excludeShape?: number[][]): Piece => {
const keys = Object.keys(TETROMINOS) as TetrominoKey[];
const key = keys[Math.floor(Math.random() * keys.length)];
const tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key];
let key = keys[Math.floor(Math.random() * keys.length)];
let tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key];
// Ensure we don't generate the same shape twice in a row
if (excludeShape && JSON.stringify(tetromino.shape) === JSON.stringify(excludeShape)) {
// Try up to 7 times to get a different shape
for (let attempts = 0; attempts < 7; attempts++) {
key = keys[Math.floor(Math.random() * keys.length)];
tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key];
if (JSON.stringify(tetromino.shape) !== JSON.stringify(excludeShape)) break;
}
}
return { shape: tetromino.shape, color: tetromino.color, x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2), y: 0 };
};
@@ -93,21 +104,29 @@ const Tetris = () => {
const [lines, setLines] = useState(0);
// 2P state
const [player1, setPlayer1] = useState<PlayerState>({
board: createBoard(),
piece: randomTetromino(),
nextPiece: randomTetromino(),
score: 0,
lines: 0,
gameOver: false,
const [player1, setPlayer1] = useState<PlayerState>(() => {
const p1Piece = randomTetromino();
const p1Next = randomTetromino(false, p1Piece.shape);
return {
board: createBoard(),
piece: p1Piece,
nextPiece: p1Next,
score: 0,
lines: 0,
gameOver: false,
};
});
const [player2, setPlayer2] = useState<PlayerState>({
board: createBoard(),
piece: randomTetromino(true),
nextPiece: randomTetromino(true),
score: 0,
lines: 0,
gameOver: false,
const [player2, setPlayer2] = useState<PlayerState>(() => {
const p2Piece = randomTetromino(true);
const p2Next = randomTetromino(true, p2Piece.shape);
return {
board: createBoard(),
piece: p2Piece,
nextPiece: p2Next,
score: 0,
lines: 0,
gameOver: false,
};
});
const [winner, setWinner] = useState<string | null>(null);
@@ -364,7 +383,7 @@ const Tetris = () => {
else playSound('click');
const newTetromino = prev.nextPiece;
const nextNext = randomTetromino(isP2);
const nextNext = randomTetromino(isP2, prev.nextPiece.shape);
if (!isValidMove(newTetromino, clearedBoard)) {
playSound('error');
@@ -625,6 +644,20 @@ const Tetris = () => {
{player1.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
</div>
{/* P1 Next Piece */}
<div className="border-2 border-primary/50 p-2 bg-background/50 flex flex-col items-center justify-center min-h-[60px]">
<p className="font-pixel text-[10px] text-foreground/60">P1 NEXT</p>
<div className="flex flex-col gap-1">
{player1.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-primary box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
</div>
{/* Center controls */}
<div className="flex flex-col gap-2 min-w-[100px] items-center">
<div className="border border-primary/50 p-2 bg-background/50 text-center">
@@ -657,6 +690,20 @@ const Tetris = () => {
</div>
{player2.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
</div>
{/* P2 Next Piece */}
<div className="border-2 border-purple-500/50 p-2 bg-background/50 flex flex-col items-center justify-center min-h-[60px]">
<p className="font-pixel text-[10px] text-purple-400">P2 NEXT</p>
<div className="flex flex-col gap-1">
{player2.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-purple-400 box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
</div>
</>
)}
+15
View File
@@ -0,0 +1,15 @@
{/* P2 Next Piece */}
<div className="border-2 border-purple-500/50 p-3 bg-background/50 flex flex-col items-center justify-center min-h-[60px]">
<p className="font-pixel text-[10px] text-purple-400">P2 NEXT</p>
<div className="flex flex-col gap-1">
{player2.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-purple-400 box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
</div>
{/* Center controls */}