[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>俄罗斯方块 - 夜未央魔改版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Courier New', monospace;
overflow: hidden;
padding: 10px;
}
.game-wrapper {
transform-origin: top center;
}
.game-body {
background: linear-gradient(145deg, #2d2d2d, #1a1a1a);
border-radius: 24px;
padding: 40px 35px;
box-shadow:
0 0 0 5px #1a1a1a,
0 0 0 10px #3d3d3d,
0 0 50px rgba(0,0,0,0.8),
inset 0 3px 15px rgba(255,255,255,0.1);
position: relative;
}
.game-body::before {
content: 'TETRIS';
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
font-weight: bold;
color: #666;
letter-spacing: 6px;
text-shadow: 0 2px 3px rgba(0,0,0,0.5);
}
.screen-frame {
background: #4a4a4a;
border-radius: 12px;
padding: 20px;
box-shadow:
inset 0 0 30px rgba(0,0,0,0.8),
0 3px 8px rgba(0,0,0,0.5);
}
.screen {
background: #9bbc0f;
border-radius: 6px;
padding: 15px;
box-shadow: inset 0 0 40px rgba(0,0,0,0.3);
position: relative;
}
.screen::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 3px,
rgba(0,0,0,0.03) 3px,
rgba(0,0,0,0.03) 6px
);
pointer-events: none;
border-radius: 6px;
}
.game-container {
display: flex;
gap: 20px;
}
.main-area {
display: flex;
flex-direction: column;
gap: 12px;
}
#game-canvas {
background: #9bbc0f;
border: 3px solid #306230;
image-rendering: pixelated;
display: block;
}
.side-panel {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 120px;
}
.info-box {
background: #8bac0f;
border: 3px solid #306230;
padding: 10px;
text-align: center;
}
.info-box .label {
font-size: 12px;
color: #306230;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: bold;
}
.info-box .value {
font-size: 22px;
font-weight: bold;
color: #0f380f;
font-family: 'Courier New', monospace;
}
.info-box .sub-value {
font-size: 14px;
color: #4a6a2a;
margin-top: 2px;
font-weight: bold;
}
#next-canvas {
background: #8bac0f;
border: 3px solid #306230;
display: block;
}
.controls-section {
margin-top: 25px;
display: flex;
justify-content: space-between;
align-items: center;
}
.d-pad {
position: relative;
width: 160px;
height: 160px;
}
.d-pad::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50px;
height: 50px;
background: #1a1a1a;
border-radius: 50%;
z-index: 1;
}
.d-pad-btn {
position: absolute;
background: linear-gradient(145deg, #3d3d3d, #2d2d2d);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.d-pad-btn:active {
transform: scale(0.95);
filter: brightness(1.5);
}
.d-pad-up {
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 50px;
height: 50px;
border-radius: 8px 8px 0 0;
}
.d-pad-down {
bottom: 8px;
left: 50%;
transform: translateX(-50%);
width: 50px;
height: 50px;
border-radius: 0 0 8px 8px;
}
.d-pad-left {
left: 8px;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
border-radius: 8px 0 0 8px;
}
.d-pad-right {
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
border-radius: 0 8px 8px 0;
}
.buttons-section {
display: flex;
gap: 15px;
position: relative;
width: 200px;
height: 120px;
justify-content: center;
}
.btn-a, .btn-b, .btn-c {
position: absolute;
width: 55px;
height: 55px;
background: linear-gradient(145deg, #8b0000, #5c0000);
border-radius: 50%;
border: none;
cursor: pointer;
box-shadow:
0 6px 12px rgba(0,0,0,0.5),
inset 0 3px 6px rgba(255,255,255,0.2);
font-size: 18px;
font-weight: bold;
color: #ff6666;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.btn-a {
bottom: 10px;
left: 10px;
}
.btn-b {
bottom: 35px;
left: 72px;
}
.btn-c {
bottom: 60px;
left: 134px;
}
.btn-a:active, .btn-b:active, .btn-c:active {
box-shadow:
0 3px 6px rgba(0,0,0,0.5),
inset 0 3px 6px rgba(0,0,0,0.3);
transform: scale(0.95);
}
.start-select {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: center;
flex-wrap: wrap;
align-items: center;
}
.btn-ss {
width: 65px;
height: 18px;
background: linear-gradient(145deg, #3d3d3d, #2d2d2d);
border: none;
border-radius: 9px;
cursor: pointer;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
font-size: 10px;
color: #888;
letter-spacing: 1px;
}
.btn-ss:active {
transform: scale(0.95);
}
.btn-reset {
width: 65px;
height: 18px;
background: linear-gradient(145deg, #4a3d2d, #3d2d1d);
border: none;
border-radius: 9px;
cursor: pointer;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
font-size: 10px;
color: #aa8866;
letter-spacing: 1px;
}
.btn-reset:active {
transform: scale(0.95);
}
.btn-rank {
width: 65px;
height: 18px;
background: linear-gradient(145deg, #2d3d2d, #1d2d1d);
border: none;
border-radius: 9px;
cursor: pointer;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
font-size: 10px;
color: #66aa66;
letter-spacing: 1px;
}
.btn-rank:active {
transform: scale(0.95);
}
.title-section {
text-align: center;
margin-bottom: 20px;
}
.game-title {
font-size: 24px;
font-weight: bold;
color: #9bbc0f;
text-shadow: 0 0 15px rgba(155,188,15,0.6);
letter-spacing: 4px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.blink {
animation: blink 1s infinite;
}
.speaker {
position: absolute;
right: 35px;
bottom: 35px;
width: 70px;
height: 70px;
background: linear-gradient(145deg, #2d2d2d, #1a1a1a);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
font-size: 14px;
font-weight: bold;
color: #666;
}
.speaker:active {
transform: scale(0.95);
}
.speaker-volume {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.85);
color: #9bbc0f;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
white-space: nowrap;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 10;
}
.speaker-volume.show {
opacity: 1;
}
.speaker-grille {
display: grid;
grid-template-columns: repeat(4, 8px);
gap: 4px;
}
.speaker-hole {
width: 8px;
height: 8px;
background: #0a0a0a;
border-radius: 50%;
}
.speaker-hole.active {
background: #4a4a4a;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(155, 188, 15, 0.92);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 6px;
z-index: 10;
padding: 20px;
}
.overlay h2 {
color: #0f380f;
font-size: 32px;
margin-bottom: 15px;
text-shadow: 3px 3px 0 rgba(0,0,0,0.1);
font-weight: bold;
}
.overlay p {
color: #306230;
font-size: 16px;
margin-bottom: 10px;
text-align: center;
font-weight: bold;
}
.overlay .final-score {
color: #0f380f;
font-size: 26px;
font-weight: bold;
margin: 15px 0;
}
.mobile-controls {
display: none;
margin-top: 25px;
justify-content: center;
gap: 15px;
}
@media (max-width: 700px) {
.mobile-controls {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 0 15px;
margin-top: 15px;
}
.controls-section {
display: none;
}
.game-body {
padding: 15px 10px;
}
.screen-frame {
padding: 8px;
}
.screen {
padding: 4px;
}
.game-container {
gap: 8px;
}
.side-panel {
min-width: 50px;
gap: 4px;
}
.info-box {
padding: 4px;
}
.info-box .label {
font-size: 9px;
}
.info-box .value {
font-size: 16px;
}
.info-box .sub-value {
font-size: 10px;
}
#next-canvas {
width: 40px;
height: 40px;
}
.start-select {
gap: 8px;
margin-top: 8px;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
}
.btn-ss, .btn-reset, .btn-rank {
width: 55px;
height: 14px;
font-size: 9px;
}
.speaker {
width: 40px;
height: 40px;
right: 10px;
bottom: 10px;
}
.speaker-grille {
grid-template-columns: repeat(4, 5px);
gap: 2px;
}
.speaker-hole {
width: 5px;
height: 5px;
}
.speaker-volume {
font-size: 11px;
}
.title-section {
margin-bottom: 10px;
}
.game-title {
font-size: 18px;
}
.mobile-dpad {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.mobile-abt-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.mobile-row {
display: flex;
gap: 6px;
justify-content: center;
}
.mobile-btn {
width: 50px;
height: 50px;
background: linear-gradient(145deg, #3d3d3d, #2d2d2d);
border: none;
border-radius: 12px;
color: #9bbc0f;
font-size: 20px;
cursor: pointer;
box-shadow: 0 5px 10px rgba(0,0,0,0.5);
}
.mobile-btn:active {
transform: scale(0.92);
}
.mobile-vol {
width: 40px;
height: 40px;
font-size: 18px;
}
.settings-btn {
width: 30px;
height: 30px;
font-size: 16px;
top: 8px;
right: 55px;
}
}
@media (min-width: 701px) {
.settings-btn {
display: flex;
align-items: center;
justify-content: center;
}
}
.mobile-btn:active {
transform: scale(0.92);
}
.mobile-row {
display: flex;
gap: 8px;
}
.settings-btn {
position: absolute;
top: 12px;
right: 55px;
width: 36px;
height: 36px;
background: linear-gradient(145deg, #3d3d3d, #2d2d2d);
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
color: #888;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
display: none;
}
.settings-btn:hover {
color: #9bbc0f;
}
@media (max-width: 700px) {
body {
padding: 5px;
}
.game-body {
padding: 12px 8px;
border-radius: 16px;
}
.game-body::before {
top: 6px;
font-size: 10px;
}
.title-section {
margin-bottom: 6px;
}
.game-title {
font-size: 16px;
}
.screen-frame {
padding: 5px;
}
.screen {
padding: 3px;
}
.game-container {
gap: 5px;
}
.main-area {
flex: 1;
}
.side-panel {
min-width: 40px;
gap: 2px;
}
.info-box {
padding: 2px;
}
.info-box .label {
font-size: 7px;
}
.info-box .value {
font-size: 13px;
}
.info-box .sub-value {
font-size: 8px;
}
#next-canvas {
width: 32px;
height: 32px;
}
.start-select {
gap: 5px;
margin-top: 5px;
}
.btn-ss, .btn-reset, .btn-rank {
width: 45px;
height: 11px;
font-size: 7px;
}
.speaker {
width: 30px;
height: 30px;
right: 6px;
bottom: 6px;
}
.speaker-grille {
grid-template-columns: repeat(4, 4px);
gap: 1px;
}
.speaker-hole {
width: 4px;
height: 4px;
}
.speaker-volume {
font-size: 9px;
}
.settings-btn {
width: 26px;
height: 26px;
font-size: 13px;
top: 5px;
right: 45px;
}
.mobile-controls {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 0 8px;
margin-top: 8px;
}
.controls-section {
display: none;
}
.mobile-dpad {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}
.mobile-abt-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.mobile-row {
display: flex;
gap: 4px;
}
.mobile-btn {
width: 42px;
height: 42px;
background: linear-gradient(145deg, #3d3d3d, #2d2d2d);
border: none;
border-radius: 10px;
color: #9bbc0f;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.mobile-btn:active {
transform: scale(0.92);
}
.mobile-vol {
width: 32px;
height: 32px;
font-size: 14px;
}
}
@media (min-width: 701px) {
.settings-btn {
display: flex;
align-items: center;
justify-content: center;
}
}
.mobile-btn:active {
transform: scale(0.92);
}
.mobile-row {
display: flex;
gap: 8px;
}
.settings-btn {
position: absolute;
top: 12px;
right: 55px;
width: 36px;
height: 36px;
background: linear-gradient(145deg, #3d3d3d, #2d2d2d);
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
color: #888;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
display: none;
}
.settings-btn:hover {
color: #9bbc0f;
}
@media (min-width: 701px) {
.settings-btn {
display: flex;
align-items: center;
justify-content: center;
}
}
.settings-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #2d2d2d;
border-radius: 12px;
padding: 25px;
z-index: 100;
box-shadow: 0 10px 40px rgba(0,0,0,0.8);
display: none;
min-width: 400px;
max-height: 80vh;
overflow-y: auto;
}
.settings-panel.active {
display: block;
}
.settings-panel h3 {
color: #9bbc0f;
margin-bottom: 20px;
text-align: center;
font-size: 24px;
font-weight: bold;
}
.settings-section {
margin-bottom: 20px;
}
.settings-section-title {
color: #888;
font-size: 13px;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid #4a4a4a;
padding-bottom: 5px;
font-weight: bold;
}
.settings-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 8px 0;
}
.settings-row label {
color: #aaa;
font-size: 15px;
min-width: 90px;
font-weight: bold;
}
.settings-row .key-display {
background: #1a1a1a;
padding: 10px 18px;
border-radius: 6px;
color: #9bbc0f;
font-size: 14px;
min-width: 90px;
text-align: center;
cursor: pointer;
border: 2px solid #4a4a4a;
font-weight: bold;
}
.settings-row .key-display:hover {
border-color: #9bbc0f;
}
.settings-row .key-display.listening {
border-color: #ff6600;
color: #ff6600;
}
.settings-panel .btn-reset-defaults {
width: 100%;
padding: 14px;
background: #3d3d2d;
border: none;
border-radius: 8px;
color: #ffaa66;
cursor: pointer;
font-size: 15px;
font-weight: bold;
margin-bottom: 10px;
}
.settings-panel .btn-reset-defaults:hover {
background: #5a5a4a;
}
.settings-panel .btn-close {
width: 100%;
padding: 16px;
background: #4a4a4a;
border: none;
border-radius: 8px;
color: #9bbc0f;
cursor: pointer;
font-size: 16px;
font-weight: bold;
margin-top: 15px;
}
.settings-panel .btn-close:hover {
background: #5a5a5a;
}
.settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 99;
display: none;
}
.settings-overlay.active {
display: block;
}
.leaderboard-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
display: none;
}
.leaderboard-overlay.active {
display: flex;
}
.leaderboard-panel {
background: linear-gradient(145deg, #2d2d2d, #1a1a1a);
border-radius: 16px;
padding: 30px;
min-width: 350px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 50px rgba(0,0,0,0.8);
}
.leaderboard-panel h2 {
color: #9bbc0f;
text-align: center;
margin-bottom: 20px;
font-size: 28px;
font-weight: bold;
}
.leaderboard-list {
list-style: none;
}
.leaderboard-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid #3d3d3d;
color: #aaa;
font-size: 15px;
font-weight: bold;
}
.leaderboard-item:first-child {
color: #ffd700;
font-weight: bold;
}
.leaderboard-item:nth-child(2) {
color: #c0c0c0;
}
.leaderboard-item:nth-child(3) {
color: #cd7f32;
}
.leaderboard-rank {
min-width: 35px;
font-weight: bold;
}
.leaderboard-name {
flex: 1;
text-align: center;
margin: 0 15px;
font-weight: bold;
}
.leaderboard-score {
min-width: 90px;
text-align: right;
font-weight: bold;
}
.leaderboard-empty {
text-align: center;
color: #666;
padding: 30px;
font-size: 16px;
font-weight: bold;
}
.leaderboard-btn {
width: 100%;
padding: 16px;
background: #4a4a4a;
border: none;
border-radius: 8px;
color: #9bbc0f;
cursor: pointer;
font-size: 16px;
font-weight: bold;
margin-top: 20px;
}
.name-input-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
display: none;
}
.name-input-overlay.active {
display: flex;
}
.name-input-panel {
background: linear-gradient(145deg, #2d2d2d, #1a1a1a);
border-radius: 16px;
padding: 30px;
min-width: 320px;
text-align: center;
box-shadow: 0 10px 50px rgba(0,0,0,0.8);
}
.name-input-panel h3 {
color: #9bbc0f;
margin-bottom: 20px;
font-size: 24px;
font-weight: bold;
}
.name-input {
width: 100%;
padding: 14px;
background: #1a1a1a;
border: 2px solid #4a4a4a;
border-radius: 8px;
color: #9bbc0f;
font-size: 18px;
font-family: 'Courier New', monospace;
text-align: center;
margin-bottom: 15px;
font-weight: bold;
}
.name-input:focus {
outline: none;
border-color: #9bbc0f;
}
.name-input-btn {
padding: 14px 35px;
background: #8b0000;
border: none;
border-radius: 8px;
color: #fff;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
.name-input-panel p {
color: #aaa;
font-size: 15px;
font-weight: bold;
margin-bottom: 15px;
}
.name-input {
width: 100%;
padding: 12px;
background: #1a1a1a;
border: 2px solid #4a4a4a;
border-radius: 8px;
color: #9bbc0f;
font-size: 16px;
font-family: 'Courier New', monospace;
text-align: center;
margin-bottom: 15px;
}
.name-input:focus {
outline: none;
border-color: #9bbc0f;
}
.name-input-btn {
padding: 12px 30px;
background: #8b0000;
border: none;
border-radius: 8px;
color: #fff;
cursor: pointer;
font-size: 14px;
}
.effect-text {
position: absolute;
font-size: 20px;
font-weight: bold;
color: #0f380f;
text-shadow: 2px 2px 0 #9bbc0f;
pointer-events: none;
z-index: 50;
animation: effectPop 1s ease-out forwards;
}
@keyframes effectPop {
0% { transform: scale(0.5); opacity: 1; }
50% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(1) translateY(-30px); opacity: 0; }
}
</style>
</head>
<body>
<div class="game-wrapper" id="gameWrapper">
<div class="game-body">
<button class="settings-btn" id="settingsBtn">⚙</button>
<div class="title-section">
<div class="game-title">俄罗斯方块 - 夜未央魔改版</div>
</div>
<div class="screen-frame">
<div class="screen">
<div class="game-container">
<div class="main-area">
<canvas id="game-canvas"></canvas>
</div>
<div class="side-panel">
<div class="info-box">
<div class="label">分数</div>
<div class="value" id="score">0</div>
</div>
<div class="info-box">
<div class="label">等级</div>
<div class="value" id="level">1</div>
<div class="sub-value" id="speed">速度: 1.0x</div>
</div>
<div class="info-box">
<div class="label">行数</div>
<div class="value" id="lines">0</div>
</div>
<div class="info-box">
<div class="label">最高</div>
<div class="value" id="highscore">0</div>
</div>
<div class="info-box">
<div class="label">下一个</div>
<canvas id="next-canvas"></canvas>
</div>
</div>
</div>
<div id="start-overlay" class="overlay">
<h2>俄罗斯方块</h2>
<h2>夜未央魔改版</h2>
<p>按 开始 键开始游戏</p>
<p class="blink">▼</p>
</div>
<div id="gameover-overlay" class="overlay" style="display: none;">
<h2>游戏结束</h2>
<div class="final-score">得分: <span id="final-score">0</span></div>
<p>按 开始 键重新开始</p>
</div>
</div>
</div>
<div class="controls-section">
<div class="d-pad">
<button class="d-pad-btn d-pad-up" id="key-up">▲</button>
<button class="d-pad-btn d-pad-down" id="key-down">▼</button>
<button class="d-pad-btn d-pad-left" id="key-left">◀</button>
<button class="d-pad-btn d-pad-right" id="key-right">▶</button>
</div>
<div class="buttons-section">
<button class="btn-a" id="btn-a">A</button>
<button class="btn-b" id="btn-b">B</button>
<button class="btn-c" id="btn-c">C</button>
</div>
</div>
<div class="start-select">
<button class="btn-reset" id="btn-reset">重置</button>
<button class="btn-ss" id="btn-select">暂停</button>
<button class="btn-ss" id="btn-start">开始</button>
<button class="btn-rank" id="btn-rank">排行</button>
</div>
<div class="speaker" id="speaker">
<div class="speaker-volume" id="volumeDisplay">100%</div>
<div class="speaker-grille">
<div class="speaker-hole active"></div>
<div class="speaker-hole active"></div>
<div class="speaker-hole active"></div>
<div class="speaker-hole active"></div>
<div class="speaker-hole active"></div>
<div class="speaker-hole active"></div>
<div class="speaker-hole active"></div>
<div class="speaker-hole active"></div>
</div>
</div>
<div class="mobile-controls">
<div class="mobile-dpad">
<div class="mobile-row">
<button class="mobile-btn" id="m-up">↑</button>
</div>
<div class="mobile-row">
<button class="mobile-btn" id="m-left">←</button>
<button class="mobile-btn" id="m-down">↓</button>
<button class="mobile-btn" id="m-right">→</button>
</div>
</div>
<div class="mobile-abt-group">
<div class="mobile-row">
<button class="mobile-btn" id="m-a">A</button>
<button class="mobile-btn" id="m-b">B</button>
<button class="mobile-btn" id="m-c">C</button>
</div>
<div class="mobile-row">
<button class="mobile-btn mobile-vol" id="m-vol-down">-</button>
<button class="mobile-btn mobile-vol" id="m-vol-up">+</button>
</div>
</div>
</div>
</div>
</div>
<div class="settings-overlay" id="settingsOverlay"></div>
<div class="settings-panel" id="settingsPanel">
<h3>按键设置</h3>
<div class="settings-section">
<div class="settings-section-title">移动控制</div>
<div class="settings-row">
<label>向左移动</label>
<div class="key-display" data-action="left">A</div>
</div>
<div class="settings-row">
<label>向右移动</label>
<div class="key-display" data-action="right">D</div>
</div>
<div class="settings-row">
<label>向下移动</label>
<div class="key-display" data-action="down">S</div>
</div>
<div class="settings-row">
<label>旋转</label>
<div class="key-display" data-action="up">W</div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">功能按钮</div>
<div class="settings-row">
<label>J(左旋)</label>
<div class="key-display" data-action="btnA">J</div>
</div>
<div class="settings-row">
<label>K(右旋)</label>
<div class="key-display" data-action="btnB">K</div>
</div>
<div class="settings-row">
<label>L(硬降)</label>
<div class="key-display" data-action="btnC">L</div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">游戏控制</div>
<div class="settings-row">
<label>开始游戏</label>
<div class="key-display" data-action="start">Enter</div>
</div>
<div class="settings-row">
<label>暂停</label>
<div class="key-display" data-action="pause">空格</div>
</div>
<div class="settings-row">
<label>重置</label>
<div class="key-display" data-action="reset">R</div>
</div>
<div class="settings-row">
<label>排行榜</label>
<div class="key-display" data-action="leaderboard">H</div>
</div>
<div class="settings-row">
<label>设置</label>
<div class="key-display" data-action="settings">O</div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">音乐设置</div>
<div class="settings-row">
<label>选择音乐</label>
<select id="musicSelect" style="background:#1a1a1a;color:#9bbc0f;padding:8px 12px;border-radius:6px;border:2px solid #4a4a4a;font-size:13px;min-width:120px;cursor:pointer;">
<option value="">苍天黄土.mp3</option>
</select>
</div>
<div class="settings-row">
<label>上传音乐</label>
<input type="file" id="musicFileInput" accept="audio/*" style="display:none;">
<button id="uploadMusicBtn" style="background:#1a1a1a;color:#9bbc0f;padding:8px 12px;border-radius:6px;border:2px solid #4a4a4a;font-size:12px;cursor:pointer;">上传</button>
</div>
<div class="settings-row">
<label>音乐音量</label>
<input type="range" id="musicVolume" min="0" max="100" value="100" style="width:120px;cursor:pointer;">
</div>
</div>
<button class="btn-reset-defaults" id="resetDefaultsBtn" style="width:100%;padding:14px;background:#4a4a4a;border:none;border-radius:8px;color:#9bbc0f;cursor:pointer;font-size:15px;font-weight:bold;margin-bottom:10px;">恢复默认值</button>
<button class="btn-close" id="closeSettings">关闭</button>
</div>
<div class="name-input-overlay" id="nameInputOverlay">
<div class="name-input-panel">
<h3>游戏结束!</h3>
<p style="color:#aaa;margin-bottom:15px;">得分为 <span id="nameInputScore" style="color:#9bbc0f;font-weight:bold;">0</span></p>
<input type="text" class="name-input" id="playerNameInput" placeholder="输入你的名字" maxlength="10">
<button class="name-input-btn" id="submitName">提交</button>
</div>
</div>
<div class="leaderboard-overlay" id="leaderboardOverlay">
<div class="leaderboard-panel">
<h2>排行榜</h2>
<ul class="leaderboard-list" id="leaderboardList"></ul>
<button class="leaderboard-btn" id="closeLeaderboard">关闭</button>
</div>
</div>
<script>
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const nextCanvas = document.getElementById('next-canvas');
const nextCtx = nextCanvas.getContext('2d');
const COLS = 10;
const ROWS = 18;
let BLOCK_SIZE = 40;
const PIECE_TYPES = ['bomb', 'gem', 'shield', 'ingot', 'lightning', 'black'];
const PIECE_COLORS = {
bomb: '#ff4444',
gem: '#44ff44',
shield: '#4488ff',
ingot: '#ff8800',
lightning: '#aa44ff',
black: '#333333'
};
const PIECE_TEXT = {
bomb: '炸',
gem: '石',
shield: '时',
ingot: '变',
lightning: '⚡',
black: ''
};
const SHAPES = {
I: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
O: [[1,1],[1,1]],
T: [[0,1,0],[1,1,1],[0,0,0]],
S: [[0,1,1],[1,1,0],[0,0,0]],
Z: [[1,1,0],[0,1,1],[0,0,0]],
J: [[1,0,0],[1,1,1],[0,0,0]],
L: [[0,0,1],[1,1,1],[0,0,0]]
};
const PIECE_NAMES = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];
let board = [];
let currentPiece = null;
let currentX = 0;
let currentY = 0;
let nextPiece = null;
let score = 0;
let level = 1;
let lines = 0;
let highScore = localStorage.getItem('tetrisHighScore') || 0;
let gameOver = false;
let gameStarted = false;
let dropInterval = 1000;
let lastDrop = 0;
let isPaused = false;
let wasPausedBeforeOverlay = false;
let speedModifier = 1;
let speedModifierEnd = 0;
let isDownHeld = false;
let isLeftHeld = false;
let isRightHeld = false;
let lastMoveTime = 0;
let moveDelay = 150;
let moveStartTime = 0;
let initialMoveDelay = 200;
const DEFAULT_KEYS = {
up: 'KeyW',
down: 'KeyS',
left: 'KeyA',
right: 'KeyD',
rotateLeft: 'KeyJ',
rotateRight: 'KeyK',
btnA: 'KeyJ',
btnB: 'KeyK',
btnC: 'KeyL',
start: 'Enter',
pause: 'Space',
reset: 'KeyR',
leaderboard: 'KeyH',
settings: 'KeyO'
};
let keyBindings = JSON.parse(localStorage.getItem('tetrisKeyBindings')) || {...DEFAULT_KEYS};
let volume = 1.0;
let volumeDisplayTimeout = null;
let audioCtx = null;
let musicPlaying = false;
let musicInterval = null;
let musicInitialized = false;
let musicVolume = 0.5;
let bgMusic = null;
let availableMusicFiles = [];
let musicPausedAt = 0;
const MUSIC_CANG_TIAN_HUANG_TU = [
{ freq: 196, dur: 0.4 },
{ freq: 262, dur: 0.2 },
{ freq: 294, dur: 0.2 },
{ freq: 330, dur: 0.4 },
{ freq: 294, dur: 0.2 },
{ freq: 262, dur: 0.2 },
{ freq: 247, dur: 0.4 },
{ freq: 220, dur: 0.2 },
{ freq: 262, dur: 0.2 },
{ freq: 294, dur: 0.6 },
{ freq: 262, dur: 0.4 },
{ freq: 247, dur: 0.2 },
{ freq: 220, dur: 0.2 },
{ freq: 196, dur: 0.6 },
{ freq: 220, dur: 0.4 },
{ freq: 262, dur: 0.2 },
{ freq: 294, dur: 0.2 },
{ freq: 330, dur: 0.4 },
{ freq: 294, dur: 0.2 },
{ freq: 262, dur: 0.2 },
{ freq: 247, dur: 0.4 },
{ freq: 294, dur: 0.2 },
{ freq: 262, dur: 0.2 },
{ freq: 220, dur: 0.6 },
{ freq: 196, dur: 0.4 },
{ freq: 220, dur: 0.2 },
{ freq: 262, dur: 0.2 },
{ freq: 294, dur: 0.4 },
{ freq: 330, dur: 0.4 },
{ freq: 294, dur: 0.4 },
{ freq: 262, dur: 0.4 },
{ freq: 247, dur: 0.4 },
{ freq: 220, dur: 0.6 }
];
let musicNoteIndex = 0;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playTone(freq, duration, type = 'square', vol = 0.3) {
if (!audioCtx || volume === 0) return;
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = type;
oscillator.frequency.setValueAtTime(freq, audioCtx.currentTime);
gainNode.gain.setValueAtTime(vol * volume, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + duration);
}
function playMoveSound() {
playTone(200, 0.05, 'square', 0.15);
}
function playRotateSound() {
playTone(350, 0.08, 'square', 0.15);
}
function playDropSound() {
playTone(150, 0.1, 'square', 0.2);
}
function playClearSound() {
playTone(523, 0.1, 'square', 0.2);
setTimeout(() => playTone(659, 0.1, 'square', 0.2), 80);
setTimeout(() => playTone(784, 0.15, 'square', 0.2), 160);
}
function playGameOverSound() {
playTone(200, 0.3, 'sawtooth', 0.2);
setTimeout(() => playTone(150, 0.3, 'sawtooth', 0.2), 200);
setTimeout(() => playTone(100, 0.5, 'sawtooth', 0.2), 400);
}
function playEffectSound(type) {
switch(type) {
case 'bomb':
playTone(80, 0.4, 'sawtooth', 0.3);
break;
case 'gem':
playTone(800, 0.2, 'sine', 0.2);
setTimeout(() => playTone(1000, 0.2, 'sine', 0.2), 80);
break;
case 'shield':
playTone(300, 0.3, 'triangle', 0.2);
break;
case 'ingot':
playTone(500, 0.15, 'sine', 0.15);
setTimeout(() => playTone(700, 0.15, 'sine', 0.15), 60);
break;
case 'lightning':
playTone(120, 0.5, 'sawtooth', 0.3);
break;
}
}
function playMusicNote() {
if (!musicPlaying || volume === 0) return;
if (bgMusic && bgMusic.paused === false) return;
const note = MUSIC_CANG_TIAN_HUANG_TU[musicNoteIndex];
playTone(note.freq, note.dur * 0.9, 'square', 0.12 * musicVolume);
musicNoteIndex = (musicNoteIndex + 1) % MUSIC_CANG_TIAN_HUANG_TU.length;
}
function playBgMusic() {
if (!bgMusic || bgMusic.paused) return;
playMusicNote();
}
function startMusic() {
if (bgMusic && bgMusic.paused === false) return;
initAudio();
musicPlaying = true;
const musicFile = localStorage.getItem('tetrisMusicFile') || '苍天黄土.mp3';
if (bgMusic) {
bgMusic.pause();
bgMusic = null;
}
bgMusic = new Audio(musicFile);
bgMusic.loop = true;
bgMusic.volume = musicVolume;
bgMusic.play().catch(() => {});
musicInterval = setInterval(playBgMusic, 400);
}
function stopMusic() {
musicPlaying = false;
if (musicInterval) {
clearInterval(musicInterval);
musicInterval = null;
}
if (bgMusic) {
musicPausedAt = bgMusic.currentTime;
bgMusic.pause();
}
}
function updateMusicVolume(val) {
musicVolume = val;
if (bgMusic) {
bgMusic.volume = musicVolume;
}
localStorage.setItem('tetrisMusicVolume', musicVolume);
}
function loadAvailableMusic() {
const musicSelect = document.getElementById('musicSelect');
const savedMusic = localStorage.getItem('tetrisMusicFile') || '苍天黄土.mp3';
availableMusicFiles = ['苍天黄土.mp3'];
musicSelect.innerHTML = '';
availableMusicFiles.forEach(file => {
const option = document.createElement('option');
option.value = file;
option.textContent = file;
option.selected = file === savedMusic;
musicSelect.appendChild(option);
});
}
function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 700;
}
function setupCanvasSize() {
if (isMobile()) {
BLOCK_SIZE = 18;
canvas.width = COLS * BLOCK_SIZE;
canvas.height = ROWS * BLOCK_SIZE;
nextCanvas.width = 50;
nextCanvas.height = 50;
} else {
BLOCK_SIZE = 40;
canvas.width = COLS * BLOCK_SIZE;
canvas.height = ROWS * BLOCK_SIZE;
nextCanvas.width = 80;
nextCanvas.height = 80;
}
}
function setupScale() {
const wrapper = document.getElementById('gameWrapper');
const gameBody = document.querySelector('.game-body');
if (!gameBody || !wrapper) return;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
if (screenWidth <= 700) {
wrapper.style.transform = 'scale(1)';
return;
}
const gameWidth = gameBody.offsetWidth;
const gameHeight = gameBody.offsetHeight;
if (gameWidth === 0 || gameHeight === 0) return;
if (screenWidth < gameWidth || screenHeight < gameHeight) {
const scaleX = (screenWidth * 0.95) / gameWidth;
const scaleY = (screenHeight * 0.95) / gameHeight;
const scale = Math.min(scaleX, scaleY, 1);
wrapper.style.transform = `scale(${scale})`;
} else {
wrapper.style.transform = 'scale(1)';
}
}
function getRandomPieceType() {
const rand = Math.random();
if (rand < 0.6) return PIECE_TYPES[Math.floor(Math.random() * 5)];
return 'black';
}
document.getElementById('highscore').textContent = highScore;
function initBoard() {
board = [];
for (let r = 0; r < ROWS; r++) {
board[r] = [];
for (let c = 0; c < COLS; c++) {
board[r][c] = { type: null };
}
}
}
function createPiece(type) {
const shape = SHAPES[type].map(row => [...row]);
const cells = [];
for (let r = 0; r < shape.length; r++) {
for (let c = 0; c < shape[r].length; c++) {
if (shape[r][c]) {
cells.push(getRandomPieceType());
}
}
}
return { type, shape, cells };
}
function getRandomPiece() {
const type = PIECE_NAMES[Math.floor(Math.random() * PIECE_NAMES.length)];
return createPiece(type);
}
function drawPieceGraphic(ctx, x, y, type, size) {
const color = PIECE_COLORS[type];
const padding = 2;
const text = PIECE_TEXT[type];
ctx.fillStyle = color;
ctx.fillRect(x + padding, y + padding, size - padding * 2, size - padding * 2);
ctx.fillStyle = '#000';
ctx.fillRect(x + padding, y + padding, size - padding * 2, 2);
ctx.fillRect(x + padding, y + padding, 2, size - padding * 2);
ctx.fillStyle = '#fff';
ctx.globalAlpha = 0.3;
ctx.fillRect(x + size - padding - 2, y + padding, 2, size - padding * 2);
ctx.fillRect(x + padding, y + size - padding - 2, size - padding * 2, 2);
ctx.globalAlpha = 1;
if (text) {
ctx.fillStyle = '#fff';
ctx.font = `bold ${size * 0.45}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, x + size/2, y + size/2);
}
}
function drawBlock(ctx, x, y, type, size = BLOCK_SIZE) {
drawPieceGraphic(ctx, x * size, y * size, type, size);
}
function drawBoard() {
ctx.fillStyle = '#9bbc0f';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c].type) {
drawBlock(ctx, c, r, board[r][c].type);
}
}
}
ctx.strokeStyle = '#306230';
ctx.lineWidth = 1;
for (let r = 0; r <= ROWS; r++) {
ctx.beginPath();
ctx.moveTo(0, r * BLOCK_SIZE);
ctx.lineTo(canvas.width, r * BLOCK_SIZE);
ctx.stroke();
}
for (let c = 0; c <= COLS; c++) {
ctx.beginPath();
ctx.moveTo(c * BLOCK_SIZE, 0);
ctx.lineTo(c * BLOCK_SIZE, canvas.height);
ctx.stroke();
}
}
function drawNextPiece() {
nextCtx.fillStyle = '#8bac0f';
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
if (!nextPiece) return;
const blockSize = isMobile() ? 12 : 18;
const offsetX = (nextCanvas.width - nextPiece.shape[0].length * blockSize) / 2;
const offsetY = (nextCanvas.height - nextPiece.shape.length * blockSize) / 2;
let cellIndex = 0;
for (let r = 0; r < nextPiece.shape.length; r++) {
for (let c = 0; c < nextPiece.shape[r].length; c++) {
if (nextPiece.shape[r][c]) {
const type = nextPiece.cells[cellIndex++];
drawPieceGraphic(nextCtx, offsetX + c * blockSize, offsetY + r * blockSize, type, blockSize);
}
}
}
}
function validMove(piece, x, y) {
for (let r = 0; r < piece.shape.length; r++) {
for (let c = 0; c < piece.shape[r].length; c++) {
if (piece.shape[r][c]) {
const newX = x + c;
const newY = y + r;
if (newX < 0 || newX >= COLS || newY >= ROWS) {
return false;
}
if (newY >= 0 && board[newY][newX].type) {
return false;
}
}
}
}
return true;
}
function rotateMatrixCW(matrix) {
const n = matrix.length;
const result = [];
for (let i = 0; i < n; i++) {
result[i] = [];
for (let j = 0; j < n; j++) {
result[i][j] = matrix[n - 1 - j][i];
}
}
return result;
}
function rotateMatrixCCW(matrix) {
const n = matrix.length;
const result = [];
for (let i = 0; i < n; i++) {
result[i] = [];
for (let j = 0; j < n; j++) {
result[i][j] = matrix[j][n - 1 - i];
}
}
return result;
}
function rotateLeft() {
if (!currentPiece || gameOver || isPaused) return;
const rotated = rotateMatrixCCW(currentPiece.shape);
const original = currentPiece.shape;
currentPiece.shape = rotated;
if (!validMove(currentPiece, currentX, currentY)) {
currentPiece.shape = original;
} else {
playRotateSound();
}
}
function rotateRight() {
if (!currentPiece || gameOver || isPaused) return;
const rotated = rotateMatrixCW(currentPiece.shape);
const original = currentPiece.shape;
currentPiece.shape = rotated;
if (!validMove(currentPiece, currentX, currentY)) {
currentPiece.shape = original;
} else {
playRotateSound();
}
}
function showEffectText(text, x, y) {
const effectEl = document.createElement('div');
effectEl.className = 'effect-text';
effectEl.textContent = text;
effectEl.style.left = (x * BLOCK_SIZE + 50) + 'px';
effectEl.style.top = (y * BLOCK_SIZE + 100) + 'px';
document.querySelector('.screen').appendChild(effectEl);
setTimeout(() => effectEl.remove(), 1000);
}
function triggerBombEffect(count) {
const size = count + 2;
const centerX = Math.floor(Math.random() * (COLS - size));
const centerY = Math.floor(Math.random() * (ROWS - size));
let cleared = 0;
for (let r = Math.max(0, centerY - 1); r < Math.min(ROWS, centerY + size); r++) {
for (let c = Math.max(0, centerX - 1); c < Math.min(COLS, centerX + size); c++) {
if (board[r][c].type) {
board[r][c] = { type: null };
cleared++;
}
}
}
playEffectSound('bomb');
showEffectText(`炸弹${size}x${size}!`, centerX, centerY);
return cleared;
}
function triggerGemEffect(count) {
let added = 0;
for (let i = 0; i < count; i++) {
const blackCells = [];
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c].type === 'black') {
blackCells.push({r, c});
}
}
}
if (blackCells.length > 0) {
const cell = blackCells[Math.floor(Math.random() * blackCells.length)];
board[cell.r][cell.c] = { type: PIECE_TYPES[Math.floor(Math.random() * 5)] };
added++;
}
}
playEffectSound('gem');
showEffectText(`绿宝石+${added}!`, 5, 5);
return added;
}
function triggerShieldEffect(count) {
const duration = count * 1000;
speedModifier = 2.0;
speedModifierEnd = Date.now() + duration;
playEffectSound('shield');
showEffectText(`减速${count}秒!`, 5, 5);
}
function triggerIngotEffect(count) {
let changed = 0;
for (let i = 0; i < count; i++) {
const x = Math.floor(Math.random() * COLS);
const y = Math.floor(Math.random() * ROWS);
if (board[y][x].type && board[y][x].type !== 'black') {
board[y][x] = { type: PIECE_TYPES[Math.floor(Math.random() * 5)] };
changed++;
}
}
playEffectSound('ingot');
showEffectText(`换图${changed}!`, 5, 5);
}
function triggerLightningEffect(count) {
let cleared = 0;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c].type === 'lightning') {
board[r][c] = { type: null };
cleared++;
}
}
}
playEffectSound('lightning');
showEffectText(`闪电${cleared}!`, 5, 5);
return cleared;
}
function processLineEffects(clearedTypes) {
let extraScore = 0;
const counts = {};
for (const type of clearedTypes) {
if (type !== 'black') {
counts[type] = (counts[type] || 0) + 1;
}
}
let effectCount = 0;
for (const [type, count] of Object.entries(counts)) {
const triggerCount = Math.floor(count / 3);
for (let i = 0; i < triggerCount; i++) {
effectCount++;
switch(type) {
case 'bomb':
extraScore += triggerBombEffect(3);
break;
case 'gem':
extraScore += triggerGemEffect(1);
extraScore += 1;
break;
case 'shield':
triggerShieldEffect(1);
break;
case 'ingot':
triggerIngotEffect(1);
break;
case 'lightning':
extraScore += triggerLightningEffect(1);
break;
}
}
}
extraScore += effectCount * 10;
return extraScore;
}
function lockPiece() {
if (!currentPiece) return;
let cellIndex = 0;
for (let r = 0; r < currentPiece.shape.length; r++) {
for (let c = 0; c < currentPiece.shape[r].length; c++) {
if (currentPiece.shape[r][c]) {
if (currentY + r < 0) {
endGame();
return;
}
board[currentY + r][currentX + c] = { type: currentPiece.cells[cellIndex] };
cellIndex++;
}
}
}
currentPiece = null;
clearLines();
spawnPiece();
}
function clearLines() {
let cleared = 0;
let clearedTypes = [];
for (let r = ROWS - 1; r >= 0; r--) {
if (board[r].every(cell => cell.type !== null)) {
const rowTypes = [];
for (let c = 0; c < COLS; c++) {
rowTypes.push(board[r][c].type);
}
clearedTypes.push(...rowTypes);
board.splice(r, 1);
board.unshift(Array(COLS).fill(0).map(() => ({ type: null })));
cleared++;
r++;
}
}
if (cleared > 0) {
const points = [0, 10, 25, 40, 60];
score += points[cleared];
const extraScore = processLineEffects(clearedTypes);
score += extraScore;
let gemBonus = 0;
const gemCounts = {};
for (const type of clearedTypes) {
if (type !== 'black') {
gemCounts[type] = (gemCounts[type] || 0) + 1;
}
}
for (const [type, count] of Object.entries(gemCounts)) {
if (count >= 4) {
gemBonus += count - 3;
}
}
score += gemBonus;
lines += cleared;
level = Math.min(100, Math.floor(score / 1000) + 1);
const speedBonus = 1 + (level - 1) * 0.01;
dropInterval = Math.max(100, 1000 / speedBonus);
playClearSound();
updateUI();
}
}
function spawnPiece() {
currentPiece = nextPiece || getRandomPiece();
nextPiece = getRandomPiece();
currentX = Math.floor((COLS - currentPiece.shape[0].length) / 2);
currentY = 0;
if (!validMove(currentPiece, currentX, currentY)) {
endGame();
}
drawNextPiece();
}
function updateUI() {
document.getElementById('score').textContent = score;
document.getElementById('level').textContent = level;
document.getElementById('lines').textContent = lines;
const currentSpeed = (1 + (level - 1) * 0.01).toFixed(2) + 'x';
document.getElementById('speed').textContent = '速度: ' + currentSpeed;
if (score > highScore) {
highScore = score;
localStorage.setItem('tetrisHighScore', highScore);
document.getElementById('highscore').textContent = highScore;
}
}
function endGame() {
gameOver = true;
gameStarted = false;
stopMusic();
if (bgMusic) {
bgMusic.pause();
}
playGameOverSound();
document.getElementById('final-score').textContent = score;
document.getElementById('gameover-overlay').style.display = 'flex';
setTimeout(() => {
document.getElementById('nameInputScore').textContent = score;
const lastName = localStorage.getItem('tetrisLastName') || '';
document.getElementById('playerNameInput').value = lastName;
document.getElementById('playerNameInput').focus();
document.getElementById('playerNameInput').select();
document.getElementById('nameInputOverlay').classList.add('active');
}, 500);
}
function saveToLeaderboard(name, gameScore) {
localStorage.setItem('tetrisLastName', name);
let leaderboard = JSON.parse(localStorage.getItem('tetrisLeaderboard')) || [];
leaderboard.push({ name, score: gameScore, date: new Date().toISOString() });
leaderboard.sort((a, b) => b.score - a.score);
leaderboard = leaderboard.slice(0, 10);
localStorage.setItem('tetrisLeaderboard', JSON.stringify(leaderboard));
}
function showLeaderboard() {
const leaderboard = JSON.parse(localStorage.getItem('tetrisLeaderboard')) || [];
const listEl = document.getElementById('leaderboardList');
if (leaderboard.length === 0) {
listEl.innerHTML = '<li class="leaderboard-empty">暂无记录</li>';
} else {
listEl.innerHTML = leaderboard.map((entry, i) => `
<li class="leaderboard-item">
<span class="leaderboard-rank">#${i + 1}</span>
<span class="leaderboard-name">${entry.name}</span>
<span class="leaderboard-score">${entry.score}</span>
</li>
`).join('');
}
document.getElementById('leaderboardOverlay').classList.add('active');
}
function hideLeaderboard() {
document.getElementById('leaderboardOverlay').classList.remove('active');
}
function startGame() {
initBoard();
score = 0;
level = 1;
lines = 0;
dropInterval = 1000;
gameOver = false;
gameStarted = true;
isPaused = false;
speedModifier = 1;
speedModifierEnd = 0;
document.getElementById('start-overlay').style.display = 'none';
document.getElementById('gameover-overlay').style.display = 'none';
nextPiece = getRandomPiece();
spawnPiece();
updateUI();
lastDrop = performance.now();
if (bgMusic) {
bgMusic.currentTime = musicPausedAt;
bgMusic.play().catch(() => {});
}
musicPlaying = true;
musicInterval = setInterval(playBgMusic, 400);
}
function resetGame() {
stopMusic();
if (bgMusic) {
bgMusic.pause();
musicPausedAt = 0;
const musicFile = localStorage.getItem('tetrisMusicFile') || '苍天黄土.mp3';
bgMusic = new Audio(musicFile);
bgMusic.loop = true;
bgMusic.volume = musicVolume;
}
initBoard();
score = 0;
level = 1;
lines = 0;
dropInterval = 1000;
gameOver = false;
gameStarted = false;
isPaused = false;
speedModifier = 1;
speedModifierEnd = 0;
currentPiece = null;
nextPiece = null;
document.getElementById('start-overlay').style.display = 'none';
document.getElementById('gameover-overlay').style.display = 'none';
document.getElementById('score').textContent = '0';
document.getElementById('level').textContent = '1';
document.getElementById('lines').textContent = '0';
document.getElementById('speed').textContent = '速度: 1.00x';
startGame();
if (bgMusic) {
bgMusic.play().catch(() => {});
}
}
function togglePause() {
if (!gameStarted || gameOver) return;
isPaused = !isPaused;
if (isPaused) {
stopMusic();
} else {
if (bgMusic) {
bgMusic.currentTime = musicPausedAt;
bgMusic.play().catch(() => {});
}
musicPlaying = true;
musicInterval = setInterval(playBgMusic, 400);
}
}
function moveDown() {
if (gameOver || isPaused || !currentPiece) return;
if (validMove(currentPiece, currentX, currentY + 1)) {
currentY++;
} else {
lockPiece();
}
}
function moveLeft() {
if (gameOver || isPaused || !currentPiece) return;
if (validMove(currentPiece, currentX - 1, currentY)) {
currentX--;
playMoveSound();
}
}
function moveRight() {
if (gameOver || isPaused || !currentPiece) return;
if (validMove(currentPiece, currentX + 1, currentY)) {
currentX++;
playMoveSound();
}
}
function hardDrop() {
if (gameOver || isPaused || !currentPiece) return;
while (validMove(currentPiece, currentX, currentY + 1)) {
currentY++;
}
playDropSound();
lockPiece();
updateUI();
}
function gameLoop(timestamp) {
if (Date.now() > speedModifierEnd) {
speedModifier = 1;
}
if (gameStarted && !gameOver && !isPaused) {
let effectiveInterval = dropInterval / speedModifier;
if (isDownHeld) {
effectiveInterval = 50;
}
if (timestamp - lastDrop > effectiveInterval) {
moveDown();
lastDrop = timestamp;
}
if (timestamp - lastMoveTime > moveDelay) {
if (isLeftHeld) {
if (timestamp - moveStartTime > initialMoveDelay) {
moveLeft();
lastMoveTime = timestamp;
}
} else if (isRightHeld) {
if (timestamp - moveStartTime > initialMoveDelay) {
moveRight();
lastMoveTime = timestamp;
}
}
}
}
drawBoard();
if (currentPiece && !gameOver) {
let cellIndex = 0;
for (let r = 0; r < currentPiece.shape.length; r++) {
for (let c = 0; c < currentPiece.shape[r].length; c++) {
if (currentPiece.shape[r][c]) {
const drawX = currentX + c;
const drawY = currentY + r;
if (drawY >= 0) {
drawBlock(ctx, drawX, drawY, currentPiece.cells[cellIndex]);
}
cellIndex++;
}
}
}
}
requestAnimationFrame(gameLoop);
}
function getKeyDisplayName(key) {
const keyMap = {
'ArrowLeft': '←',
'ArrowRight': '→',
'ArrowUp': '↑',
'ArrowDown': '↓',
'Space': '空格',
'Enter': 'Enter',
'Escape': 'Esc',
'Backspace': '退格',
'KeyR': 'R',
'KeyL': 'L',
'KeyH': 'H',
'KeyO': 'O',
'KeyZ': 'Z',
'KeyX': 'X',
'KeyW': 'W',
'KeyS': 'S',
'KeyA': 'A',
'KeyD': 'D',
'KeyJ': 'J',
'KeyK': 'K'
};
if (keyMap[key]) return keyMap[key];
if (key.startsWith('Key')) return key.substring(3);
if (key.startsWith('Digit')) return key.substring(5);
return key.toUpperCase();
}
function updateKeyDisplay() {
document.querySelectorAll('.key-display').forEach(el => {
const action = el.dataset.action;
if (keyBindings[action]) {
el.textContent = getKeyDisplayName(keyBindings[action]);
}
});
}
let listeningFor = null;
document.querySelectorAll('.key-display').forEach(el => {
el.addEventListener('click', () => {
if (listeningFor) {
document.querySelector(`[data-action="${listeningFor}"]`).classList.remove('listening');
}
listeningFor = el.dataset.action;
el.classList.add('listening');
el.textContent = '按下按键...';
});
});
document.addEventListener('keydown', (e) => {
if (listeningFor) {
e.preventDefault();
keyBindings[listeningFor] = e.code;
localStorage.setItem('tetrisKeyBindings', JSON.stringify(keyBindings));
const el = document.querySelector(`[data-action="${listeningFor}"]`);
el.textContent = getKeyDisplayName(e.code);
el.classList.remove('listening');
listeningFor = null;
return;
}
const nameInputOverlay = document.getElementById('nameInputOverlay');
if (nameInputOverlay.classList.contains('active')) {
if (e.code === 'Enter') {
e.preventDefault();
document.getElementById('submitName').click();
return;
}
}
const leaderboardOverlay = document.getElementById('leaderboardOverlay');
if (leaderboardOverlay.classList.contains('active')) {
if (e.code === 'Enter' || e.code === 'Escape') {
e.preventDefault();
hideLeaderboard();
return;
}
}
const settingsPanel = document.getElementById('settingsPanel');
if (settingsPanel.classList.contains('active')) {
if (e.code === 'Escape') {
e.preventDefault();
document.getElementById('closeSettings').click();
return;
}
}
const action = Object.keys(keyBindings).find(k => keyBindings[k] === e.code);
if (action) {
e.preventDefault();
switch(action) {
case 'down':
isDownHeld = true;
moveDown();
break;
case 'left':
isLeftHeld = true;
moveStartTime = performance.now();
moveLeft();
highlightKey('key-left');
break;
case 'right':
isRightHeld = true;
moveStartTime = performance.now();
moveRight();
highlightKey('key-right');
break;
case 'up': rotateLeft(); highlightKey('key-up'); break;
case 'rotateLeft': rotateLeft(); break;
case 'rotateRight': rotateRight(); break;
case 'btnA': rotateLeft(); highlightKey('btn-a'); break;
case 'btnB': rotateRight(); highlightKey('btn-b'); break;
case 'btnC': hardDrop(); highlightKey('btn-c'); break;
case 'start':
if (!gameStarted || gameOver) {
startGame();
}
break;
case 'pause': togglePause(); break;
case 'reset': resetGame(); break;
case 'leaderboard':
if (document.getElementById('leaderboardOverlay').classList.contains('active')) {
hideLeaderboard();
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
} else {
wasPausedBeforeOverlay = isPaused;
if (gameStarted && !gameOver && !isPaused) {
togglePause();
}
showLeaderboard();
}
break;
case 'settings':
if (document.getElementById('settingsPanel').classList.contains('active')) {
document.getElementById('settingsPanel').classList.remove('active');
document.getElementById('settingsOverlay').classList.remove('active');
if (listeningFor) {
document.querySelector(`[data-action="${listeningFor}"]`).classList.remove('listening');
listeningFor = null;
updateKeyDisplay();
}
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
} else {
wasPausedBeforeOverlay = isPaused;
document.getElementById('settingsPanel').classList.add('active');
document.getElementById('settingsOverlay').classList.add('active');
updateKeyDisplay();
loadAvailableMusic();
document.getElementById('musicVolume').value = Math.round(musicVolume * 100);
if (gameStarted && !gameOver && !isPaused) {
togglePause();
}
}
break;
}
}
});
function highlightKey(id) {
const el = document.getElementById(id);
if (el) {
el.style.filter = 'brightness(2)';
setTimeout(() => el.style.filter = '', 150);
}
}
document.getElementById('settingsBtn').addEventListener('click', () => {
if (document.getElementById('settingsPanel').classList.contains('active')) {
document.getElementById('settingsPanel').classList.remove('active');
document.getElementById('settingsOverlay').classList.remove('active');
if (listeningFor) {
document.querySelector(`[data-action="${listeningFor}"]`).classList.remove('listening');
listeningFor = null;
updateKeyDisplay();
}
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
} else {
wasPausedBeforeOverlay = isPaused;
document.getElementById('settingsPanel').classList.add('active');
document.getElementById('settingsOverlay').classList.add('active');
updateKeyDisplay();
loadAvailableMusic();
document.getElementById('musicVolume').value = Math.round(musicVolume * 100);
if (gameStarted && !gameOver && !isPaused) {
togglePause();
}
}
});
document.getElementById('resetDefaultsBtn').addEventListener('click', () => {
keyBindings = {...DEFAULT_KEYS};
localStorage.setItem('tetrisKeyBindings', JSON.stringify(keyBindings));
updateKeyDisplay();
});
document.getElementById('closeSettings').addEventListener('click', () => {
document.getElementById('settingsPanel').classList.remove('active');
document.getElementById('settingsOverlay').classList.remove('active');
if (listeningFor) {
document.querySelector(`[data-action="${listeningFor}"]`).classList.remove('listening');
listeningFor = null;
updateKeyDisplay();
}
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
});
document.getElementById('settingsOverlay').addEventListener('click', () => {
document.getElementById('closeSettings').click();
});
document.getElementById('btn-start').addEventListener('click', () => {
initAudio();
if (!gameStarted || gameOver) {
startGame();
}
});
document.getElementById('btn-select').addEventListener('click', togglePause);
document.getElementById('btn-reset').addEventListener('click', resetGame);
document.getElementById('btn-rank').addEventListener('click', () => {
if (document.getElementById('leaderboardOverlay').classList.contains('active')) {
hideLeaderboard();
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
} else {
wasPausedBeforeOverlay = isPaused;
if (gameStarted && !gameOver && !isPaused) {
togglePause();
}
showLeaderboard();
}
});
document.getElementById('btn-a').addEventListener('click', rotateLeft);
document.getElementById('btn-b').addEventListener('click', rotateRight);
document.getElementById('btn-c').addEventListener('click', hardDrop);
document.getElementById('key-left').addEventListener('click', moveLeft);
document.getElementById('key-right').addEventListener('click', moveRight);
document.getElementById('key-down').addEventListener('click', moveDown);
document.getElementById('key-up').addEventListener('click', rotateLeft);
document.getElementById('m-left').addEventListener('touchstart', (e) => { e.preventDefault(); isLeftHeld = true; moveLeft(); });
document.getElementById('m-left').addEventListener('touchend', (e) => { e.preventDefault(); isLeftHeld = false; });
document.getElementById('m-right').addEventListener('touchstart', (e) => { e.preventDefault(); isRightHeld = true; moveRight(); });
document.getElementById('m-right').addEventListener('touchend', (e) => { e.preventDefault(); isRightHeld = false; });
document.getElementById('m-down').addEventListener('touchstart', (e) => { e.preventDefault(); isDownHeld = true; moveDown(); });
document.getElementById('m-down').addEventListener('touchend', (e) => { e.preventDefault(); isDownHeld = false; });
document.getElementById('m-up').addEventListener('touchstart', (e) => { e.preventDefault(); rotateLeft(); });
document.getElementById('m-a').addEventListener('touchstart', (e) => { e.preventDefault(); rotateLeft(); });
document.getElementById('m-b').addEventListener('touchstart', (e) => { e.preventDefault(); rotateRight(); });
document.getElementById('m-c').addEventListener('touchstart', (e) => { e.preventDefault(); hardDrop(); });
document.getElementById('m-vol-up').addEventListener('touchstart', (e) => { e.preventDefault(); initAudio(); volume = Math.min(1, volume + 0.1); localStorage.setItem('tetrisVolume', volume); updateVolumeDisplay(); showVolume(); playTone(440, 0.1, 'square', 0.2); });
document.getElementById('m-vol-down').addEventListener('touchstart', (e) => { e.preventDefault(); initAudio(); volume = Math.max(0, volume - 0.1); localStorage.setItem('tetrisVolume', volume); updateVolumeDisplay(); showVolume(); playTone(440, 0.1, 'square', 0.2); });
function showVolume() {
const display = document.getElementById('volumeDisplay');
display.classList.add('show');
if (volumeDisplayTimeout) {
clearTimeout(volumeDisplayTimeout);
}
volumeDisplayTimeout = setTimeout(() => {
display.classList.remove('show');
}, 3000);
}
function updateVolumeDisplay() {
const percent = Math.round(volume * 100);
document.getElementById('volumeDisplay').textContent = percent + '%';
const holes = document.querySelectorAll('.speaker-hole');
const activeCount = Math.ceil(percent / 12.5);
holes.forEach((hole, i) => {
hole.classList.toggle('active', i < activeCount);
});
}
document.getElementById('speaker').addEventListener('click', () => {
initAudio();
volume = volume - 0.1;
if (volume < 0) {
volume = 1.0;
}
localStorage.setItem('tetrisVolume', volume);
updateVolumeDisplay();
showVolume();
playTone(440, 0.1, 'square', 0.2);
});
document.getElementById('speaker').addEventListener('contextmenu', (e) => {
e.preventDefault();
volume = volume + 0.1;
if (volume > 1.0) {
volume = 0;
}
localStorage.setItem('tetrisVolume', volume);
updateVolumeDisplay();
showVolume();
playTone(440, 0.1, 'square', 0.2);
});
document.getElementById('submitName').addEventListener('click', () => {
const name = document.getElementById('playerNameInput').value.trim() || '匿名';
saveToLeaderboard(name, score);
document.getElementById('nameInputOverlay').classList.remove('active');
showLeaderboard();
});
document.getElementById('playerNameInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
document.getElementById('submitName').click();
}
});
document.getElementById('closeLeaderboard').addEventListener('click', () => {
hideLeaderboard();
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
});
document.getElementById('leaderboardOverlay').addEventListener('click', (e) => {
if (e.target === document.getElementById('leaderboardOverlay')) {
hideLeaderboard();
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
}
});
document.addEventListener('keydown', (e) => {
if (e.code === 'Enter' || e.code === 'Escape') {
const leaderboardOverlay = document.getElementById('leaderboardOverlay');
if (leaderboardOverlay.classList.contains('active')) {
hideLeaderboard();
if (gameStarted && !gameOver && wasPausedBeforeOverlay) {
togglePause();
}
wasPausedBeforeOverlay = false;
}
}
});
document.addEventListener('keyup', (e) => {
const action = Object.keys(keyBindings).find(k => keyBindings[k] === e.code);
if (action === 'down') {
isDownHeld = false;
}
if (action === 'left') {
isLeftHeld = false;
}
if (action === 'right') {
isRightHeld = false;
}
});
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(setupScale, 100);
});
volume = parseFloat(localStorage.getItem('tetrisVolume')) || 1.0;
musicVolume = parseFloat(localStorage.getItem('tetrisMusicVolume')) || 0.5;
updateVolumeDisplay();
loadAvailableMusic();
const savedMusicFile = localStorage.getItem('tetrisMusicFile');
if (savedMusicFile) {
bgMusic = new Audio(savedMusicFile);
bgMusic.volume = musicVolume;
}
setupCanvasSize();
initBoard();
drawBoard();
drawNextPiece();
startMusic();
requestAnimationFrame(gameLoop);
setTimeout(setupScale, 100);
document.getElementById('musicSelect').addEventListener('change', (e) => {
const file = e.target.value;
localStorage.setItem('tetrisMusicFile', file);
if (bgMusic) {
bgMusic.pause();
}
bgMusic = new Audio(file);
bgMusic.loop = true;
bgMusic.volume = musicVolume;
if (gameStarted && !isPaused && !gameOver) {
bgMusic.play().catch(() => {});
}
});
document.getElementById('musicVolume').addEventListener('input', (e) => {
updateMusicVolume(e.target.value / 100);
});
document.getElementById('musicVolume').value = Math.round(musicVolume * 100);
document.getElementById('uploadMusicBtn').addEventListener('click', () => {
document.getElementById('musicFileInput').click();
});
document.getElementById('musicFileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
localStorage.setItem('tetrisMusicFile', url);
localStorage.setItem('tetrisMusicName', file.name);
if (bgMusic) {
bgMusic.pause();
}
bgMusic = new Audio(url);
bgMusic.loop = true;
bgMusic.volume = musicVolume;
const musicSelect = document.getElementById('musicSelect');
const option = document.createElement('option');
option.value = url;
option.textContent = file.name;
option.selected = true;
const existingOption = Array.from(musicSelect.options).find(opt => opt.textContent === file.name);
if (!existingOption) {
musicSelect.appendChild(option);
} else {
existingOption.selected = true;
}
if (gameStarted && !isPaused && !gameOver) {
bgMusic.play().catch(() => {});
}
}
});
</script>
</body>
</html>