# 01.单机功能 - 井子棋对战版

## 绘制棋盘

井子棋游戏的棋盘为三横三竖的格子

使用二维3×3或一维度1×9数组来表示棋盘  
如下代码所示

本案例选择二维数组的表示方法，更易于大家理解

```javascript
const map = [
	[0, 0, 0],
	[0, 0, 0],
	[0, 0, 0],
];

const map = [
	0, 0, 0,
	0, 0, 0,
	0, 0, 0,
];
```

遍历棋盘，创建棋盘，并设置相关样式

```javascript

// 创建一个div作为棋盘的容器
const mapDiv = document.createElement("div");

// 遍历棋盘
for (let row = 0; row < 3; row++) {
	for (let col = 0; col < 3; col++) {
		const grid = document.createElement("div"); // 创建一个div作为格子
		grid.className = "grid";    // 给格子一个样式
		mapDiv.appendChild(grid);   // 加到地图容器里
	}
	mapDiv.appendChild(document.createElement("br"));   // 换行
}
document.body.appendChild(mapDiv);  // 将该容器插入到html中完成渲染
```

```css

html, body {
    margin: 0;
    padding: 0;
    background-color: #222222; /* 给个背景，方便视觉观察 */
    width: 100%;
    height: 100%;
    display: flex; /* 居个中 */
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.grid {
    width: 100px; /* size */
    height: 100px;
    display: inline-block; /* 让div不要单独占一行 */
    background-color: darkgreen; /* 给个背景看得出是格子 */
    vertical-align: bottom; /* 可以去除行与行之间的空白 */
    border: 2px solid whitesmoke; /* 加个边框区分格子 */
}
```

完成效果如下

![img.png](img/01_01.png)

## 实现点击格子添加棋子

- 将格子都保存起来，方便之后都操作
- 添加点击事件
- 设置一个flag区分当前是白棋还是黑棋
- 根据点击格子的行列在这个格子上添加棋子，并更新棋盘信息
- 点击之后切换flag

可以直接将格子保存在map数组里  
并将行列信息，当前格子的棋子信息保存在格子里  
同时添加点击事件 修改渲染地图的代码如下，并写出点击函数

```javascript
/**
 * 当前颜色，0 啥也没有，1 白棋，2 黑棋
 */
let color = 1;  // 初始化为1表示白棋先下

for (let row = 0; row < 3; row++) {
	for (let col = 0; col < 3; col++) {
		const grid = document.createElement("div"); // 创建一个div作为格子
		grid.className = "grid";    // 给格子一个样式
		map[row][col] = grid;   // 直接保存在map里，方便维护
		grid.row = row; // 保存行列信息
		grid.col = col;
		grid.color = 0; // 表示为当前格子棋子的颜色
		grid.addEventListener("click", clickGrid);  // 添加点击事件
		mapDiv.appendChild(grid);   // 加到地图容器里
	}
	mapDiv.appendChild(document.createElement("br"));   // 换行
}
document.body.appendChild(mapDiv);  // 将该容器插入到html中完成渲染

/**
 * 点击格子
 * @param {MouseEvent} e
 */
function clickGrid(e) {
	const grid = e.target;  // 拿到点击的格子
	if (grid.color === 0) {  // 如果是空白的则干点事情
		// 给格子添加一个css样式，用于渲染棋子
		grid.className = `grid ${color === 1 ? "white" : "black"}`;
		grid.color = color; // 更新棋盘信息
		color = 3 - color;  // 交换棋权
	}
}

```

完成棋子的css部分  
利用伪元素绘制棋子

```css
.grid::before {
    content: "";
    width: 80px;
    height: 80px;
    display: block;
    border-radius: 50%;
    margin: 10px auto 0;
    box-sizing: border-box;
}

.grid.white::before {
    background-color: whitesmoke;
    border: 2px solid black;
}

.grid.black::before {
    background-color: black;
    border: 2px solid whitesmoke;
}
```

效果如下
![img.png](img/01_02.png)

## 实现局面判断

在横或竖或斜着的方向，三颗同色棋子连成一线则游戏结束且该玩家胜利  
如果棋盘上九个格子都被占，且没有完成上述条件则为和棋

- 写一个judge函数，在函数内实现局面判断
- 每下一步棋，调用一次judge

#### 分析如何判断输赢

> 棋子的flag为0或1或2
>
> 三数中一个数与其他任意两个数进行`&`运算 得到结果为0，如 `0 & 1 = 0` `1 & 2 = 0`
>
> 相同的数进行`&`运算的到其本身，如 `1 & 1 = 1` `2 & 2 = 2`
>
> 可以将每行没列三个和两个斜线的三个数进行`&`运算，结果为0则没有同色棋子连成一线，结果为1则白色棋子连成一线，为2则是黑色

#### 分析如何判断和棋

> 棋子的flag为0或1或2  
> 0乘以任何数都为0
>
> 可以将棋盘中9个格子都相乘，得出结果为0则表示没下完，如不为0且没有同色棋子连为一线则判断为和棋

```javascript
/**
 * 判断输赢和
 */
function judge() {
	let draw = map[0][0].color; // 是否和棋
	for (let i = 0; i < 3; i++) {
		let rowColor = map[i][0].color; // 计算横竖输赢
		let colColor = map[0][i].color;
		draw *= map[i][0].color * map[0][i].color;  // 计算和棋
		for (let j = 1; j < 3; j++) {
			rowColor &= map[i][j].color;    // 计算横竖输赢
			colColor &= map[j][i].color;
			draw *= map[i][j].color * map[j][i].color;  // 计算和棋
		}

		if (rowColor || colColor) {
			return gameOver(rowColor || colColor);
		}
	}

	// 计算斜线输赢
	const x1 = map[0][0].color & map[1][1].color & map[2][2].color;
	const x2 = map[2][0].color & map[1][1].color & map[0][2].color;
	if (x1 || x2) {
		return gameOver(x1 || x2);
	}

	// 判断是否和棋
	draw && gameOver(0);
}
```

#### 游戏结束弹出弹窗提示

- 在html中预制一个弹窗，并完成弹窗样式，初始化为隐藏
- 写一个gameOver函数，传入局面情况，在结束时调用
- 在gameOver函数里将弹窗显示，并修改提示文字

```html
<div id="overPanel">
    <div id="overTip"></div>
    <div id="resetBtn">重新开始</div>
</div>
```

```css
#overPanel {
    position: fixed;
    width: 330px;
    height: 330px;
    background-color: #aaaaaa;
    color: black;
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

#overTip {
    flex: 0.6;
    font-size: 40px;
    display: flex;
    align-items: center;
    align-content: center;
}

#resetBtn {
    width: 100px;
    height: 45px;
    background-color: #4d4d4d;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
}
```

```javascript
// 取出需要改变样式的dom元素
const overPanel = document.getElementById("overPanel");
const overTip = document.getElementById("overTip");
const resetBtn = document.getElementById("resetBtn");

/**
 * 游戏结束
 * @param {number} flag 结束标志 0 和棋， 1 白棋赢， 2 黑棋赢
 */
function gameOver(flag) {
	overPanel.style.display = "flex";
	switch (flag) {
		case 0:
			overTip.innerText = "和棋";
			overTip.style.color = "#00ff00";
			resetBtn.style.color = "#00ff00";
			break;

		case 1:
			overTip.innerText = "白棋胜";
			overTip.style.color = "white";
			resetBtn.style.color = "white";
			break;

		case 2:
			overTip.innerText = "黑棋胜";
			overTip.style.color = "black";
			resetBtn.style.color = "black";
			break
	}
}
```

效果如下
![img.png](img/01_03.png)

## 重设棋局

在上一步中已经写了一个重新开始按钮  
这一步要实现一个`reset`函数来重设棋局，并绑定给这个按钮

```html
<div id="overPanel">
    <div id="overTip"></div>
    <div id="resetBtn" onclick="reset()">重新开始</div>
</div>
```

```javascript
/**
 * 重设棋盘
 */
function reset() {
	// 把所有格子都恢复成初始状态
	for (let row = 0; row < 3; row++) {
		for (let col = 0; col < 3; col++) {
			map[row][col].className = "grid";
			map[row][col].color = 0;
		}
	}
	color = 1;  // 重设下棋flag为白棋
	overPanel.style.display = "none";   // 隐藏结束弹窗
}
```


到此单机版本的井子棋就已经完成了，下一节将带大家完成联机功能
