Implement 7-bag randomization system and improve mobile layout with vertical menu

This commit is contained in:
cmclark00 2025-03-25 16:58:54 -04:00
parent 40700d1636
commit 9e04ffc20a
3 changed files with 198 additions and 146 deletions

View file

@ -16,6 +16,10 @@
<div class="game-container"> <div class="game-container">
<div class="score-container"> <div class="score-container">
<div id="next-piece-preview">
<h3>Next</h3>
<canvas id="next-piece" width="120" height="120"></canvas>
</div>
<p>Score: <span id="score">0</span></p> <p>Score: <span id="score">0</span></p>
<p>Level: <span id="level">1</span></p> <p>Level: <span id="level">1</span></p>
<p>Lines: <span id="lines">0</span></p> <p>Lines: <span id="lines">0</span></p>
@ -27,10 +31,6 @@
<div class="game-wrapper"> <div class="game-wrapper">
<canvas id="tetris" width="320" height="640"></canvas> <canvas id="tetris" width="320" height="640"></canvas>
<div id="next-piece-preview">
<h3>Next</h3>
<canvas id="next-piece" width="120" height="120"></canvas>
</div>
</div> </div>
<div class="controls-info"> <div class="controls-info">

109
script.js
View file

@ -13,6 +13,10 @@ const PREVIEW_BLOCK_SIZE = 25;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
let touchControls = false; let touchControls = false;
// 7-bag randomization variables
let pieceBag = [];
let nextBag = [];
// Set canvas dimensions to match game board // Set canvas dimensions to match game board
canvas.width = COLS * BLOCK_SIZE; canvas.width = COLS * BLOCK_SIZE;
canvas.height = ROWS * BLOCK_SIZE; canvas.height = ROWS * BLOCK_SIZE;
@ -1510,11 +1514,13 @@ function drawBoard() {
// Generate random piece // Generate random piece
function randomPiece() { function randomPiece() {
let randomN = Math.floor(Math.random() * PIECES.length); // Initialize bags if they're empty
let randomTetromino = PIECES[randomN]; if (pieceBag.length === 0 && nextBag.length === 0) {
let randomIndex = Math.floor(Math.random() * randomTetromino.length); pieceBag = generateBag();
nextBag = generateBag();
}
return new Piece(randomTetromino, randomIndex, COLORS[randomN]); return getNextPieceFromBag();
} }
// Play piece movement sounds // Play piece movement sounds
@ -1844,6 +1850,7 @@ function init() {
function handleResize() { function handleResize() {
const gameContainer = document.querySelector('.game-container'); const gameContainer = document.querySelector('.game-container');
const gameWrapper = document.querySelector('.game-wrapper'); const gameWrapper = document.querySelector('.game-wrapper');
const scoreContainer = document.querySelector('.score-container');
if (isMobile || forceMobileControls) { if (isMobile || forceMobileControls) {
// Scale the canvas to fit mobile screen // Scale the canvas to fit mobile screen
@ -1855,42 +1862,42 @@ function handleResize() {
// Calculate available game area (accounting for UI elements) // Calculate available game area (accounting for UI elements)
const titleHeight = 40; // Estimate for title const titleHeight = 40; // Estimate for title
const scoreHeight = isPortrait ? 120 : 0; // In portrait, score is above/below game const scoreWidth = isPortrait ? 120 : 100; // Width for score container in portrait/landscape
const availableHeight = viewportHeight - titleHeight - scoreHeight; const availableWidth = viewportWidth - scoreWidth - 20; // Subtract score width + padding
const availableHeight = viewportHeight - titleHeight - 20; // Subtract title height + padding
// Calculate optimal dimensions while maintaining aspect ratio
const gameRatio = ROWS / COLS;
// Calculate scale based on available space
let targetWidth, targetHeight;
// For portrait: maximize width, maintain aspect ratio
if (isPortrait) { if (isPortrait) {
// Use 90% of viewport width // For portrait, prioritize fitting the width
const targetWidth = viewportWidth * 0.9; targetWidth = availableWidth * 0.95;
const targetHeight = (targetWidth / COLS) * ROWS; targetHeight = targetWidth * gameRatio;
// If height is too tall, scale down // If too tall, scale down based on height
if (targetHeight > availableHeight * 0.8) { if (targetHeight > availableHeight * 0.95) {
const scaleFactor = (availableHeight * 0.8) / targetHeight; targetHeight = availableHeight * 0.95;
canvas.style.width = `${targetWidth * scaleFactor}px`; targetWidth = targetHeight / gameRatio;
canvas.style.height = `${targetHeight * scaleFactor}px`;
} else {
canvas.style.width = `${targetWidth}px`;
canvas.style.height = `${targetHeight}px`;
} }
} } else {
// For landscape: maximize height, maintain aspect ratio // For landscape, prioritize fitting the height
else { targetHeight = availableHeight * 0.95;
// Use 75% of available height targetWidth = targetHeight / gameRatio;
const targetHeight = availableHeight * 0.75;
const targetWidth = (targetHeight / ROWS) * COLS;
// If width is too wide, scale down // If too wide, scale down based on width
if (targetWidth > viewportWidth * 0.6) { if (targetWidth > availableWidth * 0.95) {
const scaleFactor = (viewportWidth * 0.6) / targetWidth; targetWidth = availableWidth * 0.95;
canvas.style.width = `${targetWidth * scaleFactor}px`; targetHeight = targetWidth * gameRatio;
canvas.style.height = `${targetHeight * scaleFactor}px`;
} else {
canvas.style.width = `${targetWidth}px`;
canvas.style.height = `${targetHeight}px`;
} }
} }
// Apply dimensions
canvas.style.width = `${targetWidth}px`;
canvas.style.height = `${targetHeight}px`;
// Force a redraw of the nextPiece preview to fix rendering issues // Force a redraw of the nextPiece preview to fix rendering issues
if (nextPiece) { if (nextPiece) {
setTimeout(() => { setTimeout(() => {
@ -1906,10 +1913,6 @@ function handleResize() {
canvas.style.width = ''; canvas.style.width = '';
canvas.style.height = ''; canvas.style.height = '';
if (nextPieceCanvas) {
nextPieceCanvas.style.transform = 'none';
}
document.body.classList.remove('mobile-mode'); document.body.classList.remove('mobile-mode');
} }
} }
@ -2386,6 +2389,40 @@ function lockPiece() {
playSound(dropSound); playSound(dropSound);
} }
// Generate pieces using 7-bag randomization
function generateBag() {
// Create array with indices 0-6 (one for each piece type)
let bag = [0, 1, 2, 3, 4, 5, 6];
// Fisher-Yates shuffle algorithm
for (let i = bag.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[bag[i], bag[j]] = [bag[j], bag[i]]; // Swap elements
}
return bag;
}
// Get next piece from the bag
function getNextPieceFromBag() {
// If the current bag is empty, use the prepared next bag
if (pieceBag.length === 0) {
pieceBag = nextBag.slice();
// Generate new bag for next time
nextBag = generateBag();
}
// Take the first piece from the bag
const pieceIndex = pieceBag.shift();
const tetromino = PIECES[pieceIndex];
const color = COLORS[pieceIndex];
// Random rotation/orientation
const randomIndex = Math.floor(Math.random() * tetromino.length);
return new Piece(tetromino, randomIndex, color);
}
// Start the game // Start the game
window.onload = function() { window.onload = function() {
init(); init();

227
style.css
View file

@ -496,18 +496,18 @@ input[type=range]::-moz-range-thumb {
} }
.mobile-mode .game-container { .mobile-mode .game-container {
flex-direction: column; flex-direction: row;
align-items: center; align-items: flex-start;
gap: 15px; gap: 5px;
padding: 10px; padding: 10px;
max-width: 100vw; max-width: 100vw;
box-sizing: border-box; box-sizing: border-box;
margin-top: 50px; /* Reduced top margin */ margin-top: 40px; /* Reduced top margin */
} }
.mobile-mode .game-title { .mobile-mode .game-title {
font-size: 24px; font-size: 22px;
top: 10px; top: 5px;
text-shadow: 0 0 8px rgba(255, 0, 255, 0.7); text-shadow: 0 0 8px rgba(255, 0, 255, 0.7);
} }
@ -515,50 +515,142 @@ input[type=range]::-moz-range-thumb {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin: 0 auto;
position: relative;
margin-bottom: 20px; /* Add bottom margin to make room for next piece */
}
.mobile-mode #next-piece-preview {
position: absolute;
top: auto;
bottom: -70px; /* Position below the game board */
left: 50%;
transform: translateX(-50%);
margin: 0; margin: 0;
background: rgba(0, 0, 0, 0.7); order: 2; /* Move game board to the right */
z-index: 10; flex-grow: 1;
border-color: rgba(0, 255, 255, 0.5);
} }
.mobile-mode .score-container { .mobile-mode .score-container {
width: 100%; width: auto;
min-width: 100px;
max-width: 120px;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
flex-wrap: wrap;
justify-content: center;
align-items: center; align-items: center;
gap: 8px; gap: 5px;
padding: 8px; padding: 8px;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
order: 1; /* Move score container to the left */
margin-right: 5px;
border-radius: 8px;
height: auto;
align-self: stretch;
} }
.mobile-mode .score-container p { .mobile-mode .score-container p {
margin: 5px; margin: 3px 0;
font-size: 12px; font-size: 10px;
} }
.mobile-mode .game-btn { .mobile-mode .game-btn {
margin: 3px; margin: 3px;
padding: 8px 12px; padding: 6px 8px;
font-size: 10px; font-size: 9px;
width: 95%;
} }
.mobile-mode .controls-info { .mobile-mode .controls-info {
display: none; display: none;
} }
.mobile-mode #next-piece-preview {
position: relative;
top: auto;
bottom: auto;
left: auto;
right: auto;
transform: none;
margin: 0 0 10px 0;
background: rgba(0, 0, 0, 0.7);
z-index: 10;
border-color: rgba(0, 255, 255, 0.5);
width: 90%; /* Make it fit inside the score container */
box-sizing: border-box;
}
.mobile-mode #next-piece-preview h3 {
font-size: 12px;
margin-bottom: 5px;
}
.mobile-mode canvas#tetris {
max-width: 100%;
height: auto;
border-radius: 8px;
}
/* Portrait vs landscape adjustments */
@media (orientation: portrait) {
.mobile-mode .game-container {
padding-top: 35px;
height: calc(100vh - 40px);
}
.mobile-mode .score-container {
height: calc(100vh - 80px);
justify-content: flex-start;
}
.mobile-mode canvas#tetris {
height: calc(100vh - 80px);
width: auto;
object-fit: contain;
}
}
@media (orientation: landscape) {
.mobile-mode .game-container {
margin-top: 35px;
padding: 5px;
height: calc(100vh - 40px);
}
.mobile-mode .score-container {
height: calc(100vh - 50px);
max-width: 100px;
}
.mobile-mode canvas#tetris {
height: calc(100vh - 50px);
width: auto;
object-fit: contain;
}
}
/* Smaller devices */
@media (max-width: 400px) {
.mobile-mode .game-title {
font-size: 18px;
}
.mobile-mode .score-container p {
font-size: 9px;
}
.mobile-mode .score-container {
min-width: 85px;
max-width: 90px;
}
.mobile-mode .game-btn {
font-size: 8px;
padding: 5px 6px;
}
}
/* Tablet optimization */
@media (min-width: 768px) and (max-width: 1024px) {
.mobile-mode .game-container {
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
}
.mobile-mode .game-title {
font-size: 28px;
}
}
/* Touch instructions */ /* Touch instructions */
.touch-instructions { .touch-instructions {
display: none; display: none;
@ -588,81 +680,4 @@ input[type=range]::-moz-range-thumb {
.touch-instructions.fade-out { .touch-instructions.fade-out {
opacity: 0; opacity: 0;
}
/* Portrait vs landscape adjustments */
@media (orientation: portrait) {
.mobile-mode .game-container {
padding-top: 40px;
padding-bottom: 80px; /* Add bottom padding for next piece */
}
.mobile-mode canvas#tetris {
width: 90vw;
max-height: 75vh; /* Ensure game board doesn't take too much height */
height: auto;
object-fit: contain;
}
}
@media (orientation: landscape) {
.mobile-mode .game-container {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
margin-top: 40px;
padding: 5px;
}
.mobile-mode .score-container {
width: auto;
min-width: 150px;
max-width: 30%;
height: 80vh;
overflow-y: auto;
}
.mobile-mode .game-wrapper {
margin: 0;
height: 80vh;
}
.mobile-mode #next-piece-preview {
position: absolute;
top: 0;
bottom: auto;
left: 100%;
transform: none;
margin-left: 10px;
}
.mobile-mode canvas#tetris {
height: 75vh; /* Slightly reduced to ensure full visibility */
width: auto;
object-fit: contain;
}
}
/* Smaller devices */
@media (max-width: 400px) {
.mobile-mode .game-title {
font-size: 20px;
}
.mobile-mode .score-container p {
font-size: 10px;
}
}
/* Tablet optimization */
@media (min-width: 768px) and (max-width: 1024px) {
.mobile-mode .game-container {
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
}
.mobile-mode .game-title {
font-size: 28px;
}
} }