mirror of
https://github.com/cmclark00/tetris-3d.git
synced 2025-05-17 23:25:21 +01:00
Implement 7-bag randomization system and improve mobile layout with vertical menu
This commit is contained in:
parent
40700d1636
commit
9e04ffc20a
3 changed files with 198 additions and 146 deletions
|
@ -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
109
script.js
|
@ -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
227
style.css
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue