
基于DeepSeek完全不写一行代码实现一种自创类“夹挑棋”游戏——单个HTML源码文件
很久之前就想开发一款自己发明玩法的棋类游戏,暂时起个简单直接的名字,就叫“夹挑棋”吧。1、棋盘规格:默认状态7*7的棋盘,黑白双方各有一行棋子。2、移动规则:只能移动现有棋子,按照直线移动,直到遇到其他棋子,且不能跨子。3、变子规则:移动棋子后,(1)形成“夹”规则,即己方2个棋子夹住对方1个棋子,则对方这个棋子变为我方棋子;(2)形成“挑”规则,及己方1个棋子,插入对方2个棋子中间,则对方这2个
很久之前就想开发一款自己发明玩法的棋类游戏,暂时起个简单直接的名字,就叫“夹挑棋”吧。
最原本起名“7-14”,因为默认7*7的棋盘,胜利条件是把对方7个棋子变成自己的,就是14个,所以起这个名字,还有一个原因也是想模仿“2048”,简单好理解。但是这样没有体现出玩法,就暂定“夹挑棋”吧,某种程度上致敬黑白棋、五子棋,甚至围棋。
一、简单描述玩法就是:
1、棋盘规格:默认状态7*7的棋盘,黑白双方各有一行棋子。
2、移动规则:只能移动现有棋子,按照直线移动,直到遇到其他棋子,且不能跨子。
3、变子规则:移动棋子后,(1)形成“夹”规则,即己方2个棋子夹住对方1个棋子,则对方这个棋子变为我方棋子;(2)形成“挑”规则,即己方1个棋子,插入对方2个棋子中间,则对方这2个棋子变为我方棋子。
很早之前用Unity搭配C#实现了简单玩法,还做成了个手机小游戏,但只是给自己和周围亲友玩,没有发布各类平台。实现了最基本的垂直、水平方向移动和变子功能,以及一个十分简单的人机。按照原定设想,还要实现斜方向的规则,并且要有3个等级的人机AI,增加对战趣味和策略性。
但工作后实在太忙,这个事情就暂时搁置。直到最近各类AI大模型展露出代码编程开发游戏的潜力,尤其是这次Deepseek横空出世,我的这个想法又开始蠢蠢欲动。现在考虑的就是只实现玩法,也不寻求发布,用最简单、最快速、最容易部署、且最方便大家检验的模式,即用1个HTML代码文件,实现网页直接打开即玩。
经过两周工作和生活之余挤出的闲暇时间,我利用Deepseek,挑战不写一行代码,纯提需求和修改意见(游戏中所有页面、动画、UI控件等),历经多次斗智斗勇,终于完成此游戏的html代码1.0版本,并且支持移动端游玩(棋盘尽量小一些)。
在电脑上如果棋盘太大,导致棋盘被遮挡或者变形,可以修改页面大小(快捷键:Ctrl+鼠标滚轮滚动)。
二、此1.0版本基本实现了以下功能:
1、3个等级的“人机对战”:初级:随机移动;中级:贪心算法;高级:剪枝算法。其中高级AI思考时间可能比较长,尤其是棋子较多时。
2、“斜向变子”、“斜向移动”:游戏默认状态为只能水平、垂直运动以及变子,勾选此项,则立刻开启斜向规则,即可以斜方向直线移动和变子。
3、“辐射连击”:触发变子规则后,变化为己方的棋子,认同为也落下1子,可以触发后续变子规则,直到没有可变化的棋子为止(增加策略性、可玩性和“爽感”)。
4、“自定义棋盘”功能:可以自己Diy棋盘大小和棋子排布,棋盘最大19*19,双方棋子行数一致,且排布最少留有1行可移动空间。具体布局还能通过选择下方黑、白、灰(消除)棋子灵活设置。
5、“随机棋盘”:棋盘规格、棋子行数均随机生成。
6、“打乱棋盘”:将当前棋盘打乱,不改变棋盘规格和双方棋子数量。
7、“重新开始”:按照最近一次“棋盘设置”功能内确认的棋类状态重新开始(包括自定义、随即和打乱),如果要恢复默认棋盘,需要在“棋盘设置”界面选择。
8、“悔棋”功能:恢复为本次落子之前的状态。
9、存档功能:点击“保存”,记录当前棋盘状态;点击“读取”,加载最近一次保存的棋盘状态。
游戏主界面↓
标题棋盘设置选项↓
自定义棋盘界面↓
三、后续想法
1、同时还有很多其他需求没有实现,如“持续落子”功能(在勾选后,可以由一方持续落子)、人机对战增加“AI大模型对手”、以及联网对战等等;获胜条件也可以再斟酌,现行的比较简单直接,或许可以规定时间、步数、或达到某一条件(如占领对方最后一排等)。
2、也可以分阶段进行游戏:第一阶段,双方轮流落子,在整个棋盘,或者在一定区域内(如自己半区),各落一定数量的棋子,此时不触发变子规则;第二阶段,由落子的后手方先移动棋子,此时则需要按基本规则进行游戏。
3、甚至直接改变游戏模式,如模仿三消类游戏(类比“开心消消乐”),通过布满双方棋子的随机棋盘(或许可以有更多颜色或图案,可支持多个棋手游戏),通过棋手移动相邻棋子的形式,触发变子规则,达到消除对手棋子的目的。其他还可以结合泡泡龙模式、俄罗斯方块模式等,就是说通过这种简单易上手的基础规则,触类旁通,实现多种游戏效果。
以上都是一些简单想法,希望抛砖引玉,让更多感兴趣的人进行交流。因时间紧张,且不是专业人员,此版本可能还存在不少BUG,尤其是移动端,测试较少,请多多谅解,主要以学习为主。而且历经多次跟Deepseek对话修改,代码中应有不少冗余部分,请自行鉴别。
四、完整代码
整个html代码如下,直接复制进文本文件,改后缀为html即可打开。(或者CSDN页面可以直接运行打开?不清楚,反正Deepseek对话页面是可以的。之后试一下CSDN的InsCode功能。)
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>夹挑棋1.0</title>
<style>
body {
margin: 0;
padding: 10px;
touch-action: manipulation;
font-family: "微软雅黑", Arial, sans-serif;
background: #f0f0f0;
display: flex;
flex-direction: column;
height: 100vh;
}
#status {
text-align: center;
font-size: 24px;
padding: 15px;
background: linear-gradient(145deg, #ffffff, #e6e6e6);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 100;
flex-shrink: 0;
border-radius: 8px;
margin: 5px;
}
#score {
text-align: center;
padding: 10px;
background: #fff;
margin: 5px 0;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
#board-container {
flex: 1;
overflow: auto;
position: relative;
margin: 10px 0 60px;
display: flex;
justify-content: center;
align-items: center;
background: #f8f8f8;
border-radius: 10px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
margin-top: 60px !important;
align-items: flex-start;
/* 顶部对齐 */
}
#board {
border-collapse: collapse;
background: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
touch-action: none;
border-radius: 8px;
overflow: hidden;
}
td {
border: 1px solid #e0e0e0;
position: relative;
width: 40px;
height: 40px;
transition: background 0.2s;
background: #fcfcfc;
}
.piece {
width: 80%;
height: 80%;
border-radius: 50%;
position: absolute;
top: 10%;
left: 10%;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-sizing: border-box;
}
.black {
background: #333;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2),
inset 0 -2px 4px rgba(0, 0, 0, 0.2);
}
.white {
background: #fff;
border: 1px solid #d0d0d0;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1),
inset 0 -2px 4px rgba(255, 255, 255, 0.5);
}
.selected {
transform: scale(1.1);
box-shadow: 0 0 0 3px #ff4444,
0 4px 12px rgba(255, 68, 68, 0.3);
animation: selectBlink 0.5s ease-in-out 1;
}
@keyframes selectBlink {
0% {
box-shadow: 0 0 0 0px #ff4444;
}
50% {
box-shadow: 0 0 0 8px rgba(255, 68, 68, 0.3);
}
100% {
box-shadow: 0 0 0 3px #ff4444;
}
}
.last-move {
box-shadow: 0 0 0 3px #4CAF50;
}
.valid-move {
background: #c8e6c9 !important;
}
.flipping {
animation: flip 0.5s ease-in-out;
}
.move-arrow {
position: absolute;
height: 6px;
background: linear-gradient(to right,
rgba(0, 255, 136, 0.8) 0%,
rgba(0, 204, 102, 0.6) 70%,
transparent 100%);
transform-origin: 0 50%;
pointer-events: none;
border-radius: 3px;
filter: drop-shadow(0 2px 4px rgba(0, 200, 100, 0.3));
}
.move-arrow::before {
content: '';
position: absolute;
right: -10px;
top: -8px;
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 14px solid rgba(0, 204, 102, 0.8);
}
.move-arrow::after {
content: '';
position: absolute;
right: -10px;
top: -5px;
width: 0;
height: 0;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
border-left: 10px solid rgba(0, 255, 136, 0.8);
}
.clear-piece {
background: #f0f0f0;
border: 2px dashed #999 !important;
position: relative;
}
.clear-piece::after {
content: "×";
position: absolute;
font-size: 24px;
color: #666;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}
@keyframes flip {
0% {
transform: rotateY(0deg) scale(1);
}
50% {
transform: rotateY(180deg) scale(0.5);
}
100% {
transform: rotateY(360deg) scale(1);
}
}
/* 控件样式 */
#controls {
flex-shrink: 0;
background: white;
padding: 10px;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
border-radius: 8px;
margin: 5px;
}
.control-group {
display: flex;
flex-direction: column;
margin: 0 5px;
gap: 5px;
}
.center-controls {
display: flex;
flex-direction: column;
margin: 0 5px;
gap: 5px;
align-items: center;
}
button,
select,
input {
padding: 8px 10px;
margin: 2px;
border: 1px solid #ddd;
border-radius: 6px;
background: #f8f8f8;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
button:hover,
select:hover {
background: #f0f0f0;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
input[type="checkbox"] {
width: 18px;
height: 18px;
vertical-align: middle;
margin-right: 5px;
}
.modal-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 12px;
z-index: 1000;
display: none;
flex-direction: column;
gap: 12px;
min-width: 280px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
#custom-controls {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 15px;
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.1);
grid-template-columns: repeat(4, 1fr);
gap: 10px;
align-items: start;
max-height: 45vh;
overflow-y: auto;
border-radius: 12px 12px 0 0;
}
/* 自定义界面专用样式 */
#custom-controls[style*="display: grid"]~#board-container {
margin-top: 20px;
margin-bottom: 150px;
/* 加大底部留白 */
}
/* 移动端适配 */
@media (max-width: 600px) {
#board-container {
margin: 5px 0 80px;
}
#custom-controls[style*="display: grid"]~#board-container {
margin-bottom: 200px;
}
}
/* 棋盘包裹层适配 */
.board-wrapper {
width: 90%;
max-width: 90vh;
/* 防止过大 */
padding-top: 90%;
}
.custom-section {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
min-width: 120px;
}
.color-option {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
position: relative;
margin: 0 3px;
transition: transform 0.2s;
}
.color-option.selected {
transform: scale(1.15);
box-shadow: 0 0 0 2px #4CAF50;
}
.slider-group {
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
}
.slider-group label {
display: flex;
align-items: center;
justify-content: space-between;
margin: 1px 0;
}
input[type="range"] {
width: 80px;
height: 6px;
margin: 0 2px;
}
input[type="number"] {
width: 50px;
padding: 4px;
margin: 0 2px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: none;
backdrop-filter: blur(2px);
}
.color-row {
display: flex;
gap: 16px;
margin-bottom: 8px;
}
.button-group {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
/* 规则弹窗样式 */
#rule-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.9);
display: none;
/* 默认隐藏 */
justify-content: center;
align-items: center;
z-index: 10000;
/* 确保在棋盘上方 */
}
.rule-content {
background: #fff;
padding: 2rem;
border-radius: 1rem;
width: min(90%, 700px);
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
position: relative;
}
.rule-content .piece {
width: 30px;
height: 30px;
position: static;
margin: 5px;
}
.rule-section {
margin: 1.5rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.demo-row {
display: flex;
gap: 10px;
align-items: center;
margin: 1rem 0;
}
.start-btn {
background: #4CAF50;
color: white;
padding: 12px 24px;
border: none;
border-radius: 25px;
font-size: 1.1rem;
cursor: pointer;
margin-top: 1rem;
}
</style>
</head>
<body>
<div id="rule-modal" class="modal-overlay">
<div class="rule-content">
<h2>📖 游戏规则说明</h2>
<div class="rule-section">
<h3>🎯 游戏目标</h3>
<p>通过移动棋子,将对方棋子转化为己方颜色。当一方无法移动或棋子数为零时,另一方获胜。</p>
</div>
<div class="rule-section">
<h3>🚶 移动规则</h3>
<ul>
<li>横向/纵向直线移动,可跨越多个空格</li>
<li>遇到棋子必须停止</li>
<li><em>斜向移动需开启对应选项</em></li>
</ul>
</div>
<div class="rule-section">
<h3>🔄 变子规则</h3>
<div class="example">
<div class="demo-row">
<div class="piece black"></div>
<div class="piece white"></div>
<div class="piece black"></div>
<span>→</span>
<div class="piece black"></div>
<div class="piece black"></div>
<div class="piece black"></div>
</div>
<p>"夹"规则:当己方棋子在两侧时,中间敌方棋子转化</p>
</div>
<div class="example">
<div class="demo-row">
<div class="piece white"></div>
<div class="piece black"></div>
<div class="piece white"></div>
<span>→</span>
<div class="piece black"></div>
<div class="piece black"></div>
<div class="piece black"></div>
</div>
<p>"挑"规则:当己方棋子在中间时,两侧敌方棋子转化</p>
</div>
</div>
<button class="start-btn" onclick="Game.hideRules()">开始游戏</button>
</div>
</div>
<div id="status">当前回合:黑方</div>
<div id="score">黑方 0 : 0 白方</div>
<div id="board-container">
<table id="board"></table>
</div>
<!-- 主控栏 -->
<div id="controls">
<div class="control-group">
<select id="gameMode">
<option value="pve">人机对战</option>
<option value="pvp">双人对战</option>
</select>
<div id="aiLevelGroup" class="control-group">
<select id="aiLevel">
<option value="1">初级 AI</option>
<option value="2">中级 AI</option>
<option value="3">高级 AI</option>
</select>
</div>
</div>
<div class="control-group">
<button onclick="Game.showBoardSettings()">棋盘设置</button>
<button onclick="Game.showRules()">游戏玩法</button>
</div>
<div class="control-group">
<button onclick="Game.handleRestart()">重新开始</button>
<button onclick="Game.undoMove()">悔棋</button>
</div>
<div class="control-group center-controls">
<label><input type="checkbox" id="diagonalRule"> 斜向变子</label>
<label><input type="checkbox" id="diagonalMove"> 斜向移动</label>
<label><input type="checkbox" id="radiationMode" onchange="Game.config.radiation = this.checked;">
辐射连击</label>
</div>
<div class="control-group">
<button onclick="Game.saveGame()">保存</button>
<button onclick="Game.loadGame()">读取</button>
</div>
</div>
<!-- 棋盘设置菜单 -->
<div class="modal-overlay" id="board-settings-overlay"></div>
<div class="modal-dialog" id="board-settings-menu">
<button onclick="Game.startCustomBoard()">自定义棋盘</button>
<button onclick="Game.handleRandomBoard()">随机棋盘</button>
<button onclick="Game.handleShuffleBoard()">打乱棋盘</button>
<button onclick="Game.handleDefaultBoard()">默认棋盘</button>
<button onclick="Game.hideBoardSettings()">取消</button>
</div>
<!-- 自定义棋盘控制栏 -->
<div id="custom-controls">
<div class="custom-section">
<div class="slider-group">
<label>宽度:
<input type="range" id="customWidthSlider" min="3" max="19" value="7">
<input type="number" id="customWidth" min="3" max="19" value="7">
</label>
<label>高度:
<input type="range" id="customHeightSlider" min="3" max="19" value="7">
<input type="number" id="customHeight" min="3" max="19" value="7">
</label>
<label>行数:
<input type="range" id="pieceRowsSlider" min="1" max="9" value="1">
<input type="number" id="pieceRows" min="1" max="9" value="1">
</label>
</div>
</div>
<div class="custom-section">
<div class="color-row">
<div class="color-option black selected" onclick="Game.selectColor(0)"></div>
<div class="color-option white" onclick="Game.selectColor(1)"></div>
<div class="color-option clear-piece" onclick="Game.selectColor(null)"></div>
</div>
<div class="control-group">
<label><input type="radio" name="firstPlayer" value="0" checked> 黑先</label>
<label><input type="radio" name="firstPlayer" value="1"> 白先</label>
</div>
</div>
<div class="custom-section">
<div class="button-group">
<button onclick="Game.clearBoard()">清空棋盘</button>
<button onclick="Game.restoreDefault()">恢复默认</button>
</div>
</div>
<div class="custom-section">
<div class="button-group">
<button onclick="Game.confirmCustom()" style="background:#4CAF50;color:white;">确认布局</button>
<button onclick="Game.cancelCustom()" style="background:#f44336;color:white;">取消布局</button>
</div>
</div>
</div>
<!-- 游戏模式切换确认 -->
<div class="modal-overlay" id="mode-overlay" style="display:none;"></div>
<div class="modal-dialog" id="mode-dialog" style="display:none;">
<p>是否保留当前棋局?</p>
<button onclick="Game.handleModeChange('keep')">保留棋盘</button>
<button onclick="Game.handleModeChange('restart')">重新开始</button>
<button onclick="Game.handleModeChange('cancel')">取消</button>
</div>
<script>
class Game {
static config = {
mode: 'pve',
aiLevel: 1,
boardWidth: 7,
boardHeight: 7,
pieceRows: 1,
diagonalRule: false,
diagonalMove: false,
radiation: false,
moveHistory: [],
scores: [0, 0],
currentPlayer: 0,
board: [],
processing: false,
selectedPiece: null,
customizing: false,
customColor: 0,
originalState: null,
lastCustomState: null,
lastMove: null
};
static init() {
// 在初始化时显示游戏规则
if (!localStorage.getItem('rulesShown')) {
this.showRules();
localStorage.setItem('rulesShown', 'true');
}
this.resetBoard(true);
this.bindEvents();
this.updateView();
}
static showRules() {
document.getElementById('rule-modal').style.display = 'flex';
document.body.style.overflow = 'hidden'; // 禁止背景滚动
}
static hideRules() {
document.getElementById('rule-modal').style.display = 'none';
document.body.style.overflow = 'auto';
}
static getCurrentState() {
return {
boardWidth: this.config.boardWidth,
boardHeight: this.config.boardHeight,
pieceRows: this.config.pieceRows,
board: JSON.parse(JSON.stringify(this.config.board)),
currentPlayer: this.config.currentPlayer,
firstPlayer: this.config.currentPlayer
};
}
static resetBoard(initial = false) {
const { boardWidth, boardHeight, pieceRows } = this.config;
// 自动修正行数值
const validRows = Math.min(pieceRows, Math.floor((boardHeight - 1) / 2));
this.config.pieceRows = Math.max(validRows, 1);
// 初始化棋盘数组
this.config.board = Array.from({ length: boardHeight }, (_, i) =>
Array.from({ length: boardWidth }, (_, j) => {
if (initial) {
if (i < this.config.pieceRows) return 1; // 顶部白棋
if (i >= boardHeight - this.config.pieceRows) return 0; // 底部黑棋
}
return null; // 中间区域为空
})
);
// 强制刷新视图
this.updateView();
}
static updateView() {
// 尺寸合法性检查
this.config.boardWidth = Math.max(3, Math.min(19, this.config.boardWidth));
this.config.boardHeight = Math.max(3, Math.min(19, this.config.boardHeight));
const boardElement = document.getElementById('board');
const { boardWidth, boardHeight, board, lastMove } = this.config;
boardElement.innerHTML = '';
for (let i = 0; i < boardHeight; i++) {
const row = boardElement.insertRow();
for (let j = 0; j < boardWidth; j++) {
const cell = row.insertCell();
cell.dataset.row = i;
cell.dataset.col = j;
cell.onclick = (e) => this.handleClick(i, j);
if (board[i][j] !== null) {
const piece = document.createElement('div');
piece.className = `piece ${board[i][j] ? 'white' : 'black'}`;
if (lastMove && lastMove[0] === i && lastMove[1] === j) {
piece.classList.add('last-move');
}
cell.appendChild(piece);
}
}
}
document.getElementById('score').textContent =
`黑方 ${this.config.scores[0]} : ${this.config.scores[1]} 白方`;
document.getElementById('status').textContent =
`当前回合:${this.config.currentPlayer ? '白方' : '黑方'}`;
const container = document.getElementById('board-container');
const cellWidth = Math.min(container.offsetWidth / boardWidth - 2, 60);
const cellHeight = Math.min(container.offsetHeight / boardHeight - 2, 60);
document.querySelectorAll('#board td').forEach(td => {
td.style.width = `${cellWidth}px`;
td.style.height = `${cellHeight}px`;
});
}
static handleClick(row, col) {
if (this.config.processing) return;
if (this.config.customizing) {
this.config.board[row][col] = this.config.customColor;
this.updateView();
return;
}
if (this.config.board[row][col] === this.config.currentPlayer) {
this.selectPiece(row, col);
}
else if (this.config.selectedPiece &&
document.querySelector(`[data-row="${row}"][data-col="${col}"]`).classList.contains('valid-move')) {
this.movePiece(row, col);
}
}
static selectPiece(row, col) {
this.clearSelection();
this.config.selectedPiece = { row, col };
document.querySelector(`[data-row="${row}"][data-col="${col}"] .piece`)
.classList.add('selected');
this.showValidMoves(row, col);
}
static showValidMoves(row, col) {
this.clearValidMoves();
const directions = this.config.diagonalMove ?
[[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]] :
[[-1, 0], [1, 0], [0, -1], [0, 1]];
directions.forEach(([dx, dy]) => {
let steps = 1;
while (true) {
const newRow = row + dx * steps;
const newCol = col + dy * steps;
if (!this.isValid([newRow, newCol])) break;
if (this.config.board[newRow][newCol] !== null) break;
const cell = document.querySelector(`[data-row="${newRow}"][data-col="${newCol}"]`);
cell.classList.add('valid-move');
steps++;
}
});
}
static async movePiece(newRow, newCol) {
const { selectedPiece, board } = this.config;
if (!selectedPiece) return;
this.config.processing = true;
this.config.moveHistory.push({
board: JSON.parse(JSON.stringify(board)),
scores: [...this.config.scores],
player: this.config.currentPlayer
});
board[selectedPiece.row][selectedPiece.col] = null;
board[newRow][newCol] = this.config.currentPlayer;
this.config.lastMove = [newRow, newCol];
const startCell = document.querySelector(`[data-row="${selectedPiece.row}"][data-col="${selectedPiece.col}"]`);
const endCell = document.querySelector(`[data-row="${newRow}"][data-col="${newCol}"]`);
this.createPathAnimation(startCell, endCell);
this.updateView();
await this.processConversion(newRow, newCol);
this.config.currentPlayer = 1 - this.config.currentPlayer;
this.clearSelection();
this.updateView();
if (this.config.mode === 'pve' && this.config.currentPlayer === 1) {
setTimeout(() => this.aiTurn(), 500);
}
await this.checkGameOver();
this.config.processing = false;
}
static createPathAnimation(startCell, endCell) {
const getCenter = cell => {
const rect = cell.getBoundingClientRect();
return {
x: rect.left + rect.width / 2 + window.scrollX,
y: rect.top + rect.height / 2 + window.scrollY
};
};
const start = getCenter(startCell);
const end = getCenter(endCell);
const dx = end.x - start.x;
const dy = end.y - start.y;
const angle = Math.atan2(dy, dx);
const distance = Math.hypot(dx, dy);
const arrow = document.createElement('div');
arrow.className = 'move-arrow';
arrow.style.cssText = `
width: ${distance}px;
left: ${start.x}px;
top: ${start.y}px;
transform: rotate(${angle}rad);
transform-origin: 0 50%;
`;
document.body.appendChild(arrow);
setTimeout(() => arrow.remove(), 300);
}
static async processConversion(startRow, startCol) {
const radiationMode = this.config.radiation;
const currentPlayer = this.config.currentPlayer; // 锁定当前玩家状态
const enemyPlayer = 1 - currentPlayer;
let totalFlipped = [];
// 使用广度优先搜索处理连锁反应
const queue = [{ row: startRow, col: startCol }];
const visited = new Set();
while (queue.length > 0) {
const { row, col } = queue.shift();
const key = `${row},${col}`;
if (visited.has(key)) continue;
visited.add(key);
// 获取当前棋子的所有可转换目标
const flipped = this.checkConversions(row, col);
if (flipped.length > 0) {
await this.flipPieces(flipped);
totalFlipped.push(...flipped);
// 更新分数(实时更新)
this.config.scores[currentPlayer] += flipped.length;
this.updateView();
// 如果启用辐射模式,将新转换的棋子加入队列
if (radiationMode) {
flipped.forEach(([r, c]) => {
queue.push({ row: r, col: c });
});
}
}
}
return totalFlipped.length > 0;
}
static checkConversions(row, col) {
const directions = this.config.diagonalRule ?
[[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]] :
[[-1, 0], [1, 0], [0, -1], [0, 1]];
const current = this.config.currentPlayer;
const enemy = 1 - current;
const piecesToFlip = [];
for (const [dx, dy] of directions) {
// "挑"规则检查
const front = { row: row + dx, col: col + dy };
const back = { row: row - dx, col: col - dy };
if (this.isValid(front) && this.isValid(back) &&
this.getPiece(front.row, front.col) === enemy &&
this.getPiece(back.row, back.col) === enemy) {
piecesToFlip.push([front.row, front.col]);
piecesToFlip.push([back.row, back.col]);
}
// "夹"规则检查
const far = { row: row + dx * 2, col: col + dy * 2 };
if (this.isValid(far) &&
this.getPiece(far.row, far.col) === current &&
this.getPiece(row + dx, col + dy) === enemy) {
piecesToFlip.push([row + dx, col + dy]);
}
}
return piecesToFlip.filter(([r, c]) => this.getPiece(r, c) !== current);
}
// 新增辅助方法
static getPiece(row, col) {
return this.isValid({ row, col }) ? this.config.board[row][col] : null;
}
static async flipPieces(pieces) {
const promises = pieces.map(([row, col]) => {
return new Promise(resolve => {
const cell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`);
const piece = cell.querySelector('.piece');
piece.classList.add('flipping');
setTimeout(() => {
this.config.board[row][col] = this.config.currentPlayer;
piece.className = `piece ${this.config.currentPlayer ? 'white' : 'black'}`;
piece.classList.remove('flipping');
resolve();
}, 500);
});
});
await Promise.all(promises);
this.updateView();
}
static async checkGameOver() {
if (this.config.customizing) return false;
await new Promise(resolve => setTimeout(resolve, 500));
const blackCount = this.config.board.flat().filter(c => c === 0).length;
const whiteCount = this.config.board.flat().filter(c => c === 1).length;
// 完善胜负判断
const blackCanMove = this.canMove(0);
const whiteCanMove = this.canMove(1);
let gameOver = false;
if (blackCount === 0 || whiteCount === 0) {
gameOver = true;
} else if (!blackCanMove && !whiteCanMove) {
gameOver = true;
} else if (this.config.currentPlayer === 0 && !blackCanMove) {
gameOver = true;
} else if (this.config.currentPlayer === 1 && !whiteCanMove) {
gameOver = true;
}
if (gameOver) {
const winner = blackCount > whiteCount ? '黑方' : '白方';
return this.showGameOver(winner);
}
return false;
}
static showGameOver(winner) {
const msg = this.config.mode === 'pve' ?
`${winner}胜利!${winner === '黑方' ? '玩家' : 'AI'}获胜!是否重新开始?` :
`${winner}胜利!是否重新开始?`;
if (confirm(msg)) this.handleRestart();
return true;
}
static canMove(player) {
for (let i = 0; i < this.config.boardHeight; i++) {
for (let j = 0; j < this.config.boardWidth; j++) {
if (this.config.board[i][j] === player) {
if (this.getValidMoves(i, j).length > 0) return true;
}
}
}
return false;
}
static getValidMoves(row, col) {
const moves = [];
const directions = this.config.diagonalMove ?
[[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]] :
[[-1, 0], [1, 0], [0, -1], [0, 1]];
directions.forEach(([dx, dy]) => {
let steps = 1;
while (true) {
const newRow = row + dx * steps;
const newCol = col + dy * steps;
if (!this.isValid([newRow, newCol])) break;
if (this.config.board[newRow][newCol] !== null) break;
moves.push([newRow, newCol]);
steps++;
}
});
return moves;
}
static isValid(position) {
let row, col;
if (Array.isArray(position)) {
[row, col] = position;
} else {
({ row, col } = position);
}
return row >= 0 && row < this.config.boardHeight &&
col >= 0 && col < this.config.boardWidth;
}
static clearSelection() {
this.config.selectedPiece = null;
document.querySelectorAll('.selected, .valid-move').forEach(el => {
el.classList.remove('selected', 'valid-move');
});
}
static clearValidMoves() {
document.querySelectorAll('.valid-move').forEach(el => {
el.classList.remove('valid-move');
});
}
static handleShuffleBoard() {
const { board, boardWidth, boardHeight } = this.config;
const pieces = [];
for (let i = 0; i < boardHeight; i++) {
for (let j = 0; j < boardWidth; j++) {
if (board[i][j] !== null) pieces.push(board[i][j]);
board[i][j] = null;
}
}
while (pieces.length) {
const idx = Math.floor(Math.random() * pieces.length);
const [row, col] = this.getRandomEmptyCell();
board[row][col] = pieces.splice(idx, 1)[0];
}
this.config.currentPlayer = 0;
this.updateView();
this.checkGameOver();
this.config.lastCustomState = this.getCurrentState();
this.hideBoardSettings();
}
static handleRandomBoard() {
do {
this.config.boardWidth = Math.max(5, Math.floor(Math.random() * 10) + 5);
this.config.boardHeight = Math.max(5, Math.floor(Math.random() * 10) + 5);
this.config.pieceRows = Math.min(
Math.floor(Math.random() * 3) + 1,
Math.floor((this.config.boardHeight - 1) / 2)
);
} while (this.config.pieceRows < 1);
this.resetBoard(true);
this.config.currentPlayer = 0;
this.config.lastCustomState = this.getCurrentState();
this.updateView();
this.hideBoardSettings();
}
static handleDefaultBoard() {
this.config.boardWidth = 7;
this.config.boardHeight = 7;
this.config.pieceRows = 1;
this.resetBoard(true);
this.config.currentPlayer = 0;
this.config.lastCustomState = this.getCurrentState();
this.updateView();
this.hideBoardSettings();
}
static undoMove() {
if (this.config.moveHistory.length === 0) return;
const lastState = this.config.moveHistory.pop();
this.config.board = lastState.board;
this.config.scores = lastState.scores;
this.config.currentPlayer = lastState.player;
this.updateView();
}
static saveGame() {
const saveData = {
config: {
boardWidth: this.config.boardWidth,
boardHeight: this.config.boardHeight,
pieceRows: this.config.pieceRows,
mode: this.config.mode,
aiLevel: this.config.aiLevel,
currentPlayer: this.config.currentPlayer
},
board: JSON.parse(JSON.stringify(this.config.board)),
scores: [...this.config.scores]
};
localStorage.setItem('gameState', JSON.stringify(saveData));
alert('游戏已保存');
}
static loadGame() {
if (!confirm('加载存档将覆盖当前进度!')) return;
const saved = JSON.parse(localStorage.getItem('gameState'));
if (!saved) return alert('找不到存档');
Object.assign(this.config, saved.config);
this.config.board = saved.board;
this.config.scores = saved.scores;
this.updateView();
}
static aiTurn() {
const moves = this.getAllValidMoves(1);
if (moves.length === 0) return;
let selectedMove;
switch (this.config.aiLevel) {
case 1:
selectedMove = moves[Math.floor(Math.random() * moves.length)];
break;
case 2:
selectedMove = moves.reduce((best, current) =>
this.evaluateMove(current) > this.evaluateMove(best) ? current : best
);
break;
case 3:
selectedMove = this.alphaBetaSearch(3)[1];
break;
}
this.selectPiece(selectedMove[0][0], selectedMove[0][1]);
setTimeout(() => this.movePiece(selectedMove[1][0], selectedMove[1][1]), 500);
}
static getAllValidMoves(player) {
const moves = [];
for (let row = 0; row < this.config.boardHeight; row++) {
for (let col = 0; col < this.config.boardWidth; col++) {
if (this.config.board[row][col] === player) {
this.getValidMoves(row, col).forEach(target => {
moves.push([[row, col], target]);
});
}
}
}
return moves;
}
static evaluateMove(move) {
const tempBoard = JSON.parse(JSON.stringify(this.config.board));
const [start, end] = move;
tempBoard[start[0]][start[1]] = null;
tempBoard[end[0]][end[1]] = 1;
return this.calculateConversion(tempBoard, end[0], end[1]).length;
}
static alphaBetaSearch(depth) {
let alpha = -Infinity;
let beta = Infinity;
let bestMove = null;
let bestValue = -Infinity;
const moves = this.getAllValidMoves(1);
for (const move of moves) {
const originalState = this.getCurrentState();
this.simulateMove(move);
const value = this.minValue(depth - 1, alpha, beta);
this.restoreState(originalState);
if (value > bestValue) {
bestValue = value;
bestMove = move;
}
alpha = Math.max(alpha, bestValue);
}
return [bestValue, bestMove];
}
static maxValue(depth, alpha, beta) {
if (depth === 0 || this.checkGameOverSync()) {
return this.evaluateBoard();
}
let value = -Infinity;
const moves = this.getAllValidMoves(1);
for (const move of moves) {
const originalState = this.getCurrentState();
this.simulateMove(move);
value = Math.max(value, this.minValue(depth - 1, alpha, beta));
this.restoreState(originalState);
if (value >= beta) return value;
alpha = Math.max(alpha, value);
}
return value;
}
static minValue(depth, alpha, beta) {
if (depth === 0 || this.checkGameOverSync()) {
return this.evaluateBoard();
}
let value = Infinity;
const moves = this.getAllValidMoves(0);
for (const move of moves) {
const originalState = this.getCurrentState();
this.simulateMove(move);
value = Math.min(value, this.maxValue(depth - 1, alpha, beta));
this.restoreState(originalState);
if (value <= alpha) return value;
beta = Math.min(beta, value);
}
return value;
}
static checkGameOverSync() {
const blackCount = this.config.board.flat().filter(c => c === 0).length;
const whiteCount = this.config.board.flat().filter(c => c === 1).length;
return blackCount === 0 || whiteCount === 0 ||
(!this.canMove(0) && !this.canMove(1));
}
static simulateMove(move) {
const [start, end] = move;
const tempBoard = JSON.parse(JSON.stringify(this.config.board));
tempBoard[start[0]][start[1]] = null;
tempBoard[end[0]][end[1]] = 1;
const flipped = this.calculateConversion(tempBoard, end[0], end[1]);
flipped.forEach(([r, c]) => tempBoard[r][c] = 1);
this.config.board = tempBoard;
}
static evaluateBoard() {
let score = 0;
const centerWeight = 3;
const centerArea = this.getCenterArea();
for (let i = 0; i < this.config.boardHeight; i++) {
for (let j = 0; j < this.config.boardWidth; j++) {
if (this.config.board[i][j] === 1) {
score += centerArea.some(([x, y]) => x === i && y === j) ? centerWeight : 1;
} else if (this.config.board[i][j] === 0) {
score -= centerArea.some(([x, y]) => x === i && y === j) ? centerWeight : 1;
}
}
}
return score;
}
static restoreState(state) {
this.config.board = JSON.parse(JSON.stringify(state.board));
this.config.scores = state.scores ? [...state.scores] : [0, 0];
this.config.currentPlayer = state.currentPlayer;
}
static bindEvents() {
document.getElementById('gameMode').addEventListener('change', (e) => {
this.config.mode = e.target.value;
document.getElementById('aiLevelGroup').style.display =
e.target.value === 'pve' ? 'flex' : 'none';
this.updateView();
});
document.getElementById('aiLevel').addEventListener('change', (e) => {
this.config.aiLevel = parseInt(e.target.value);
});
['diagonalRule', 'diagonalMove', 'radiationMode'].forEach(id => {
document.getElementById(id).addEventListener('change', (e) => {
this.config[id] = e.target.checked;
});
});
const linkDimension = (sliderId, inputId, prop) => {
const slider = document.getElementById(sliderId);
const input = document.getElementById(inputId);
const update = (value) => {
value = parseInt(value) || 0;
// 行数有效性验证
if (prop === 'pieceRows') {
const maxRows = Math.floor((this.config.boardHeight - 1) / 2);
value = Math.min(value, maxRows);
value = Math.max(value, 1); // 至少1行
if (value !== parseInt(input.value)) {
alert(`行数已自动调整为${value}(最大允许值:${maxRows})`);
}
}
// 更新控件值
slider.value = input.value = value;
this.config[prop] = value;
// 清空棋盘逻辑
this.config.board = Array.from({ length: this.config.boardHeight }, () =>
Array(this.config.boardWidth).fill(null)
);
// 初始化基础布局
if (prop === 'pieceRows') {
this.resetBoard(true); // 带初始棋子布局
} else {
this.updateView(); // 仅更新空棋盘
}
};
slider.addEventListener('input', (e) => update(e.target.value));
input.addEventListener('change', (e) => update(e.target.value));
};
linkDimension('customWidthSlider', 'customWidth', 'boardWidth');
linkDimension('customHeightSlider', 'customHeight', 'boardHeight');
linkDimension('pieceRowsSlider', 'pieceRows', 'pieceRows');
// 自定义棋盘拖动逻辑
let isDragging = false;
document.addEventListener('mousedown', (e) => {
if (!this.config.customizing) return;
const colorBtn = e.target.closest('.color-option');
if (!colorBtn) return;
isDragging = true;
this.config.customColor = colorBtn.classList.contains('black') ? 0 :
colorBtn.classList.contains('white') ? 1 : null;
});
document.addEventListener('mouseup', () => isDragging = false);
document.querySelectorAll('#board td').forEach(cell => {
cell.addEventListener('mouseenter', (e) => {
if (isDragging && this.config.customizing) {
const row = parseInt(cell.dataset.row);
const col = parseInt(cell.dataset.col);
this.config.board[row][col] = this.config.customColor;
this.updateView();
}
});
});
}
static showBoardSettings() {
document.getElementById('board-settings-menu').style.display = 'flex';
document.getElementById('board-settings-overlay').style.display = 'block';
}
static hideBoardSettings() {
document.getElementById('board-settings-menu').style.display = 'none';
document.getElementById('board-settings-overlay').style.display = 'none';
}
static startCustomBoard() {
// 保存原始状态时包含完整棋盘数据
this.config.originalState = {
...this.getCurrentState(),
board: JSON.parse(JSON.stringify(this.config.board)) // 深度拷贝棋盘
};
document.getElementById('controls').style.display = 'none';
document.getElementById('custom-controls').style.display = 'grid';
this.hideBoardSettings();
this.config.customizing = true;
// 不再重置棋盘,保持当前状态
this.updateView(); // 只刷新视图
console.log('进入自定义模式,当前棋盘尺寸:',
`${this.config.boardWidth}x${this.config.boardHeight}`);
}
static selectColor(color) {
this.config.customColor = color;
document.querySelectorAll('.color-option').forEach(opt => {
opt.classList.toggle('selected',
(color === 0 && opt.classList.contains('black')) ||
(color === 1 && opt.classList.contains('white')) ||
(color === null && opt.classList.contains('clear-piece'))
);
});
}
static clearBoard() {
this.config.board = Array.from({ length: this.config.boardHeight },
() => Array(this.config.boardWidth).fill(null));
this.updateView();
}
static restoreDefault() {
this.config.boardWidth = 7;
this.config.boardHeight = 7;
this.config.pieceRows = 1;
this.resetBoard(true);
this.updateView();
}
static confirmCustom() {
this.config.lastCustomState = {
boardWidth: this.config.boardWidth,
boardHeight: this.config.boardHeight,
pieceRows: this.config.pieceRows,
firstPlayer: parseInt(document.querySelector('input[name="firstPlayer"]:checked').value),
board: JSON.parse(JSON.stringify(this.config.board))
};
this.config.customizing = false;
document.getElementById('custom-controls').style.display = 'none';
document.getElementById('controls').style.display = 'grid';
this.config.currentPlayer = this.config.lastCustomState.firstPlayer;
this.updateView();
// AI先手处理
if (this.config.mode === 'pve' && this.config.currentPlayer === 1) {
setTimeout(() => this.aiTurn(), 500);
}
}
static cancelCustom() {
if (this.config.originalState) {
Object.assign(this.config, this.config.originalState);
}
document.getElementById('custom-controls').style.display = 'none';
document.getElementById('controls').style.display = 'grid';
this.updateView();
}
static handleRestart() {
if (confirm('确定要重置游戏吗?')) {
if (this.config.lastCustomState) {
this.config.boardWidth = this.config.lastCustomState.boardWidth;
this.config.boardHeight = this.config.lastCustomState.boardHeight;
this.config.pieceRows = this.config.lastCustomState.pieceRows;
this.config.board = JSON.parse(JSON.stringify(this.config.lastCustomState.board));
this.config.currentPlayer = this.config.lastCustomState.firstPlayer;
} else {
this.resetBoard(true);
}
this.config.scores = [0, 0];
this.config.moveHistory = [];
this.updateView();
}
}
static getRandomEmptyCell() {
const emptyCells = [];
for (let i = 0; i < this.config.boardHeight; i++) {
for (let j = 0; j < this.config.boardWidth; j++) {
if (this.config.board[i][j] === null) {
emptyCells.push([i, j]);
}
}
}
return emptyCells[Math.floor(Math.random() * emptyCells.length)] || [0, 0];
}
static calculateConversion(tempBoard, row, col) {
const piecesToFlip = [];
const directions = this.config.diagonalRule ?
[[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]] :
[[-1, 0], [1, 0], [0, -1], [0, 1]];
const current = 1;
for (const [dx, dy] of directions) {
const front = [row + dx, col + dy];
const back = [row - dx, col - dy];
if (this.isValid(front) && this.isValid(back) &&
tempBoard[front[0]][front[1]] === 0 &&
tempBoard[back[0]][back[1]] === 0) {
piecesToFlip.push(front, back);
}
const far = [row + dx * 2, col + dy * 2];
if (this.isValid(far) &&
tempBoard[far[0]][far[1]] === current &&
tempBoard[row + dx][col + dy] === 0) {
piecesToFlip.push([row + dx, col + dy]);
}
}
return piecesToFlip;
}
static getCenterArea() {
const centerX = Math.floor(this.config.boardWidth / 2);
const centerY = Math.floor(this.config.boardHeight / 2);
return [
[centerY, centerX],
[centerY - 1, centerX], [centerY + 1, centerX],
[centerY, centerX - 1], [centerY, centerX + 1]
];
}
}
// 初始化游戏
window.addEventListener('DOMContentLoaded', () => Game.init());
</script>
</body>
</html>
更多推荐
所有评论(0)