原文链接: vue 三子棋
上一篇: 组合数学 棋盘多项式
游戏体验,使用Chrome,火狐或者edge打开,使用了grid布局,其他浏览器可能不支持
http://www.ahaoboy.cn:2222/static/apps/%E4%B8%89%E5%AD%90%E6%A3%8B/index.html#/
三子棋不是简单组合游戏,不能使用组合游戏的思路
但是由于状态数目不多 最多3^9 = 20000,可以建立状态图来求解
使用一维数组表示棋盘状态,0,1,2分别表示没有棋子,白子,黑子
使用bfs搜索所有状态并建立状态转移图,计算每个状态的出度
由出度为0的状态开始,计算状态的获胜概率和该状态是否是必胜状态
对于某一个状态,求其下一个状态时,先找是否存在必胜状态,并且去除必输状态,找其他状态中获胜概率最大的状态
一个状态的获胜概率是该状态可以到达的所有状态的获胜概率的平均值
界面代码
<template>
<div class="main">
请选择先手
<label><input type="radio" v-model="first" :value="1">白先(自己)</label>
<label><input type="radio" v-model="first" :value="2">黑先(电脑)</label>
<label><input type="checkbox" v-model="isAuto">自动下棋</label>
<div class="game">
<div class="cell" v-for="i,index in items" :key="index" @click="click(index)">
<div :class="['celltype1','celltype2'][i-1]"></div>
</div>
</div>
<h1 v-show="!isFill && !winer">请{{'白黑'[next-1]}}棋落子</h1>
<div>
<button @click="reset">reset</button>
<button @click="autoplay">autoplay</button>
</div>
<h1 v-show="winer!=0 ">{{'白黑'[winer-1]}}棋获胜</h1>
<h1 v-show="winer==0 && isFill">平局</h1>
</div>
</template>
<script>
import '../util/Board'
import {Board, GameMgr} from '../util/Board'
var gm = new GameMgr()
export default {
data() {
return {
items: Array.from(Array(9)).fill(0),
next: 1,
first: 1,
isAuto: false
}
},
name: "game-board",
computed: {
winer() {
return new Board(this.items, this.next).getWiner()
},
isFill() {
return this.items.every(item => !!item)
},
},
watch: {
first(newVal, oldVal) {
this.reset()
if (this.next == 2)
this.autoplay()
},
items(newVal, oldVal) {
if (this.winer != 0) {
console.log(this.winer, ' win')
}
},
isAuto(newVal) {
this.reset()
// if (newVal)
// this.autoplay()
}
},
methods: {
click(index) {
if (this.items[index] != 0 || !!this.winer)
return
this.$set(this.items, index, this.next)
// this.items[index] = this.next
this.next = [2, 1][this.next - 1]
if (this.winer == 0 && this.isAuto && !this.isFill)
this.autoplay()
},
reset() {
this.items = Array.from(Array(9)).fill(0)
this.next = this.first
},
autoplay() {
let b = new Board(this.items, this.next)
let n = gm.next(b)
this.items = n.state
this.next = [2, 1][this.next - 1]
},
}
}
</script>
<style scoped>
.main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
}
.game {
width: 300px;
height: 300px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.cell {
display: flex;
width: 100%;
height: 100px;
justify-content: center;
align-items: center;
background: url("../assets/cell_bg.jpg") no-repeat;
background-size: 100% 100%;
}
.celltype1 {
width: 60%;
height: 60%;
border-radius: 50%;
background: white;
}
.celltype2 {
width: 60%;
height: 60%;
border-radius: 50%;
background: black;
}
</style>
工具类代码Board.js
class Board {
// 当前棋盘状态和下一步谁下,出度
constructor(state, next = 1, degree = 0) {
this.state = Array.from(state)
this.next = next
this.degree = degree
this.win1 = 0.5 // 1赢的概率
this.win2 = 0.5
this.ret = 0 // 表示平局,1,表示1必胜,2表示2必胜
}
// 返回是否胜利
isWin(num) {
let sub = Array.from(Array(3)).fill('' + num).join('')
// 横
let str = ''
for (let i = 0; i < 3; i++) {
str += [this.state[i * 3], this.state[i * 3 + 1], this.state[i * 3 + 2]].join('') + '-'
}
// 竖
for (let i = 0; i < 3; i++) {
str += [this.state[i], this.state[i + 3], this.state[i + 6]].join('') + '-'
}
// 对角线
str += [this.state[0], this.state[4], this.state[8]].join('')
str += '-'
str += [this.state[2], this.state[4], this.state[6]].join('')
return str.indexOf(sub) != -1
}
// 0没有人胜利,1表示1胜利,2表示2胜利
getWiner() {
if (this.isWin(1))
return 1
if (this.isWin(2))
return 2
return 0
}
getWinP() {
return this['win' + this.next]
}
toString() {
return '' + this.next + '-' + this.state.join('')
}
show() {
for (let i = 0; i < 3; i++) {
console.log(this.state[i * 3], this.state[i * 3 + 1], this.state[i * 3 + 2])
}
console.log({
next: this.next,
degree: this.degree,
win1: this.win1,
win2: this.win2,
winer: this.getWiner(),
ret: this.ret
})
}
}
class GameMgr {
constructor() {
let q = []
let all_state = {}
let all_state_set = new Set()
let s1 = new Board(Array.from(Array(9)).fill(0), 1)
let s2 = new Board(Array.from(Array(9)).fill(0), 2)
q.push(s1)
q.push(s2)
all_state[s1.toString()] = s1
all_state[s2.toString()] = s2
all_state_set.add(s1.toString())
all_state_set.add(s2.toString())
// 保存图的结构
let g = {}
while (q.length > 0) {
let t = q.shift()
g[t.toString()] = []
// t.show()
if (t.getWiner() != 0) {
let winer = t.getWiner()
all_state[t.toString()]['win' + winer] = 1
continue
}
for (let i = 0; i < 9; i++) {
if (t.state[i] == 0) {
let nstate = Array.from(t.state)
nstate[i] = t.next
let b = new Board(nstate, t.next == 1 ? 2 : 1)
g[t.toString()].push(b.toString())
all_state[t.toString()].degree++
if (!all_state_set.has(b.toString())) {
q.push(b)
all_state[b.toString()] = b
all_state_set.add(b.toString())
}
}
}
}
var Heap = require('heap')
var heap = new Heap(function (a, b) {
return a.degree - b.degree
})
for (let i in all_state) {
heap.push(all_state[i])
}
while (heap.size() > 0) {
let t = heap.pop()
// 计算胜率
let len = g[t.toString()].length
if (len != 0) {
let win1 = 0
let win2 = 0
for (let i = 0; i < len; i++) {
win1 += all_state[g[t.toString()][i]].win1
win2 += all_state[g[t.toString()][i]].win2
}
all_state[t.toString()].win1 = win1 / len
all_state[t.toString()].win2 = win2 / len
// 计算必胜的情况
if (g[t.toString()].every(item => all_state[item].ret == 1) || (t.next == 1 && g[t.toString()].some(item => all_state[item].ret == 1))) {
// 所有可到达状态为1,则该节点1必胜
all_state[t.toString()].ret = 1
// t.show()
} else if (g[t.toString()].every(item => all_state[item].ret == 2) || (t.next == 2 && g[t.toString()].some(item => all_state[item].ret == 2))) {
// 所有可到达状态为2,则该节点2必胜
all_state[t.toString()].ret = 2
// t.show()
}
} else {
// 出度为0,表示终止状态,计算终止状态的胜利情况
all_state[t.toString()].ret = t.getWiner()
// t.show()
}
}
this.g = g
this.all_state = all_state
console.log('ready!')
}
// 找某一状态的下次状态’0-01021032013‘
next(b) {
b.show()
let len = this.g[b.toString()].length
if (len > 0) {
let n = {win1: 0, win2: 0}
for (let i = 0; i < len; i++) {
// t 是b可到达的下一个状态
let t = this.all_state[this.g[b.toString()][i]]
t.show()
// 是否存在下一步为先手必胜,并且该状态能到达的所有状态都不是后手必胜
if (t.ret == b.next) {
return t
}
if (t.ret == [2, 1][b.next - 1])
continue
// 下一步状态为胜率比较大,并且不能是当前先手的必败态
if (n['win' + b.next] <= t['win' + b.next])
n = t
}
return n
}
}
}
export {
Board,
GameMgr,
}