This repository has been archived on 2024-07-22. You can view files and clone it, but cannot push or open issues or pull requests.
Doric/doric-demo/src/Gobang.ts

685 lines
23 KiB
TypeScript
Raw Normal View History

import { Stack, hlayout, Group, Color, stack, layoutConfig, LayoutSpec, vlayout, Text, ViewHolder, ViewModel, VMPanel, scroller, modal, text, gravity, Gravity, View, popover } from "doric";
2019-12-16 14:40:10 +08:00
import { colors } from "./utils";
2019-12-16 14:08:30 +08:00
2019-12-17 15:45:00 +08:00
enum State {
Unspecified,
BLACK,
WHITE,
}
const count = 13
class AIComputer {
2019-12-17 16:39:59 +08:00
wins: Array<Array<{ x: number, y: number }>> = []
2019-12-17 15:45:00 +08:00
winCount = 0
2019-12-17 16:39:59 +08:00
matrix: Map<number, State>
constructor(matrix: Map<number, State>) {
this.matrix = matrix
2019-12-17 15:45:00 +08:00
for (let y = 0; y < count; y++) {
for (let x = 0; x < count - 4; x++) {
2019-12-17 16:39:59 +08:00
this.wins.push([])
2019-12-17 15:45:00 +08:00
for (let k = 0; k < 5; k++) {
2019-12-17 16:39:59 +08:00
this.wins[this.winCount].push({
x: x + k,
y,
})
2019-12-17 15:45:00 +08:00
}
this.winCount++;
}
}
for (let x = 0; x < count; x++) {
for (let y = 0; y < count - 4; y++) {
2019-12-17 16:39:59 +08:00
this.wins.push([])
2019-12-17 15:45:00 +08:00
for (let k = 0; k < 5; k++) {
2019-12-17 16:39:59 +08:00
this.wins[this.winCount].push({
x,
y: y + k,
})
2019-12-17 15:45:00 +08:00
}
this.winCount++;
}
}
2019-12-16 14:08:30 +08:00
2019-12-17 15:45:00 +08:00
for (let x = 0; x < count - 4; x++) {
for (let y = 0; y < count - 4; y++) {
2019-12-17 16:39:59 +08:00
this.wins.push([])
2019-12-17 15:45:00 +08:00
for (let k = 0; k < 5; k++) {
2019-12-17 16:39:59 +08:00
this.wins[this.winCount].push({
x: x + k,
y: y + k,
})
2019-12-17 15:45:00 +08:00
}
this.winCount++;
}
}
for (let x = 0; x < count - 4; x++) {
for (let y = count - 1; y > 3; y--) {
2019-12-17 16:39:59 +08:00
this.wins.push([])
2019-12-17 15:45:00 +08:00
for (let k = 0; k < 5; k++) {
2019-12-17 16:39:59 +08:00
this.wins[this.winCount].push({
x: x + k,
y: y - k,
})
2019-12-17 15:45:00 +08:00
}
this.winCount++;
}
}
}
2019-12-17 16:39:59 +08:00
get blackWins() {
return this.wins.map((win) => {
let idx = 0
for (let e of win) {
switch (this.matrix.get(e.x + e.y * count)) {
case State.BLACK:
idx++
break
case State.WHITE:
return 0
default:
break
}
}
return idx
})
}
get whiteWins() {
return this.wins.map((win) => {
2019-12-17 16:52:43 +08:00
let idx = 0
2019-12-17 16:39:59 +08:00
for (let e of win) {
switch (this.matrix.get(e.x + e.y * count)) {
case State.WHITE:
2019-12-17 16:52:43 +08:00
idx++
2019-12-17 16:39:59 +08:00
break
case State.BLACK:
return 0
default:
break
}
}
2019-12-17 16:52:43 +08:00
return idx
2019-12-17 16:39:59 +08:00
})
}
2019-12-17 15:45:00 +08:00
compute(matrix: State[], role: State.BLACK | State.WHITE) {
const myScore = new Array(matrix.length).fill(0)
const rivalScore = new Array(matrix.length).fill(0)
const myWins = role === State.BLACK ? this.blackWins : this.whiteWins
const rivalWins = role === State.BLACK ? this.whiteWins : this.blackWins
let max = 0
let retIdx = 0
matrix.forEach((state, idx) => {
if (state != State.Unspecified) {
return
}
2019-12-17 16:39:59 +08:00
this.wins.forEach((e, winIdx) => {
if (e.filter(e => (e.x + e.y * count) === idx).length === 0) {
return
}
switch (rivalWins[winIdx]) {
case 1:
2019-12-17 17:01:21 +08:00
rivalScore[idx] += 1
2019-12-17 16:39:59 +08:00
break
case 2:
2019-12-17 17:01:21 +08:00
rivalScore[idx] += 10
2019-12-17 16:39:59 +08:00
break
case 3:
2019-12-17 17:01:21 +08:00
rivalScore[idx] += 100
2019-12-17 16:39:59 +08:00
break
case 4:
rivalScore[idx] += 10000
break
default:
break
}
2019-12-17 15:45:00 +08:00
2019-12-17 16:39:59 +08:00
switch (myWins[winIdx]) {
case 1:
2019-12-17 17:01:21 +08:00
myScore[idx] += 2
2019-12-17 16:39:59 +08:00
break
case 2:
2019-12-17 17:01:21 +08:00
myScore[idx] += 20
2019-12-17 16:39:59 +08:00
break
case 3:
2019-12-17 17:01:21 +08:00
myScore[idx] += 200
2019-12-17 16:39:59 +08:00
break
case 4:
myScore[idx] += 20000
break
default:
break
2019-12-17 15:45:00 +08:00
}
2019-12-17 16:39:59 +08:00
})
2019-12-17 15:45:00 +08:00
if (rivalScore[idx] > max) {
max = rivalScore[idx];
retIdx = idx
} else if (rivalScore[idx] == max) {
if (myScore[idx] > myScore[retIdx]) {
retIdx = idx
}
}
if (myScore[idx] > max) {
max = myScore[idx]
retIdx = idx
} else if (myScore[idx] == max) {
if (rivalScore[idx] > rivalScore[retIdx]) {
retIdx = idx
}
}
})
return retIdx
}
}
2019-12-16 14:08:30 +08:00
const lineColor = Color.BLACK
function columLine() {
return (new Stack).apply({
layoutConfig: layoutConfig().most().configWidth(LayoutSpec.JUST),
width: 1,
backgroundColor: lineColor,
})
}
function rowLine() {
return (new Stack).apply({
layoutConfig: layoutConfig().most().configHeight(LayoutSpec.JUST),
height: 1,
backgroundColor: lineColor,
})
}
function pointer(size: number) {
return (new Stack).apply({
layoutConfig: layoutConfig().just(),
width: size,
height: size,
})
}
2019-12-16 17:06:42 +08:00
enum GameMode {
P2P,
P2C,
C2P,
}
2019-12-16 14:08:30 +08:00
interface GoBangState {
count: number
gap: number
role: "white" | "black"
matrix: Map<number, State>
2019-12-16 15:39:29 +08:00
anchor?: number
2019-12-16 17:06:42 +08:00
gameMode: GameMode
2019-12-17 17:29:36 +08:00
gameState: "blackWin" | "whiteWin" | "idle"
2019-12-16 14:08:30 +08:00
}
class GoBangVH extends ViewHolder {
root!: Group
gap = 0
2019-12-16 14:40:10 +08:00
currentRole!: Text
result!: Text
2019-12-16 15:39:29 +08:00
targetZone: View[] = []
2019-12-16 17:06:42 +08:00
gameMode!: Text
2019-12-17 17:50:51 +08:00
assistant!: Text
2019-12-16 14:08:30 +08:00
build(root: Group): void {
this.root = root
}
actualBuild(state: GoBangState): void {
const boardSize = state.gap * (state.count - 1)
const gap = state.gap
const borderWidth = gap
this.gap = state.gap
scroller(
2020-01-06 10:43:18 +08:00
vlayout(
[
text({
text: "五子棋",
layoutConfig: layoutConfig().configWidth(LayoutSpec.MOST),
textSize: 30,
textColor: Color.WHITE,
backgroundColor: colors[0],
textAlignment: gravity().center(),
height: 50,
2019-12-16 14:08:30 +08:00
}),
2020-01-06 10:43:18 +08:00
stack(
[
stack(
[
...(new Array(count - 2)).fill(0).map((_, idx) => {
return columLine().also(v => {
v.left = (idx + 1) * gap
})
}),
...(new Array(count - 2)).fill(0).map((_, idx) => {
return rowLine().also(v => {
v.top = (idx + 1) * gap
})
}),
],
{
layoutConfig: layoutConfig().just()
.configMargin({ top: borderWidth, left: borderWidth }),
width: boardSize,
height: boardSize,
border: {
width: 1,
color: lineColor,
},
}),
...this.targetZone = (new Array(count * count)).fill(0).map((_, idx) => {
const row = Math.floor(idx / count)
const colum = idx % count
return pointer(gap).also(v => {
v.top = (row - 0.5) * gap + borderWidth
v.left = (colum - 0.5) * gap + borderWidth
})
}),
],
{
layoutConfig: layoutConfig().just(),
width: boardSize + 2 * borderWidth,
height: boardSize + 2 * borderWidth,
backgroundColor: Color.parse("#E6B080"),
}
),
this.gameMode = text({
text: "游戏模式",
2019-12-16 14:40:10 +08:00
textSize: 20,
textColor: Color.WHITE,
2020-01-06 10:43:18 +08:00
layoutConfig: layoutConfig().most().configHeight(LayoutSpec.JUST),
2019-12-16 14:40:10 +08:00
height: 50,
2020-01-06 10:43:18 +08:00
backgroundColor: colors[8],
2019-12-16 14:40:10 +08:00
}),
2020-01-06 10:43:18 +08:00
hlayout(
[
this.currentRole = text({
text: "当前:",
textSize: 20,
textColor: Color.WHITE,
layoutConfig: layoutConfig().just().configWeight(1),
height: 50,
backgroundColor: colors[1],
}),
this.result = text({
text: "获胜方:",
textSize: 20,
textColor: Color.WHITE,
layoutConfig: layoutConfig().just().configWeight(1),
height: 50,
backgroundColor: colors[2],
}),
],
{
layoutConfig: layoutConfig().fit().configWidth(LayoutSpec.MOST),
}),
this.assistant = text({
text: "提示",
2019-12-16 14:40:10 +08:00
textSize: 20,
textColor: Color.WHITE,
2020-01-06 10:43:18 +08:00
layoutConfig: layoutConfig().just().configWidth(LayoutSpec.MOST),
2019-12-16 14:40:10 +08:00
height: 50,
2020-01-06 10:43:18 +08:00
backgroundColor: colors[3],
2019-12-16 14:40:10 +08:00
}),
2020-01-06 10:43:18 +08:00
],
{
2019-12-16 14:08:30 +08:00
layoutConfig: layoutConfig().fit(),
backgroundColor: Color.parse('#ecf0f1'),
2020-01-06 10:43:18 +08:00
}
)
2019-12-16 14:08:30 +08:00
).in(this.root)
}
}
class GoBangVM extends ViewModel<GoBangState, GoBangVH>{
2019-12-17 15:45:00 +08:00
computer!: AIComputer
2019-12-16 14:08:30 +08:00
onAttached(state: GoBangState, vh: GoBangVH) {
2019-12-17 16:52:43 +08:00
if (!this.computer) {
this.computer = new AIComputer(state.matrix)
}
2019-12-16 14:08:30 +08:00
vh.actualBuild(state)
2019-12-16 15:39:29 +08:00
vh.targetZone.forEach((e, idx) => {
e.onClick = () => {
2019-12-17 17:29:36 +08:00
if (state.gameState !== 'idle') {
return
}
2019-12-16 15:39:29 +08:00
const zoneState = state.matrix.get(idx)
if (zoneState === State.BLACK || zoneState === State.WHITE) {
modal(context).toast('This position had been token.')
return
}
if (state.anchor === undefined || state.anchor != idx) {
this.updateState(it => {
it.anchor = idx
})
2019-12-16 14:08:30 +08:00
} else {
2019-12-16 15:39:29 +08:00
this.updateState(it => {
if (it.role === 'black') {
it.matrix.set(idx, State.BLACK)
it.role = 'white'
} else {
it.matrix.set(idx, State.WHITE)
it.role = 'black'
}
it.anchor = undefined
2019-12-16 16:38:53 +08:00
if (this.checkResult(idx)) {
2019-12-17 17:29:36 +08:00
modal(context).toast(`恭喜获胜方${it.role === 'white' ? "黑方" : "白方"}`)
it.gameState = it.role === 'white' ? 'blackWin' : 'whiteWin'
2019-12-16 17:28:50 +08:00
} else {
if (it.role === 'black' && it.gameMode === GameMode.C2P) {
2019-12-17 16:39:59 +08:00
setTimeout(() => {
this.computeNextStep(it)
}, 0)
2019-12-16 17:28:50 +08:00
} else if (it.role === 'white' && it.gameMode === GameMode.P2C) {
2019-12-17 16:39:59 +08:00
setTimeout(() => {
this.computeNextStep(it)
}, 0)
2019-12-16 17:28:50 +08:00
}
2019-12-16 16:38:53 +08:00
}
2019-12-16 15:39:29 +08:00
})
2019-12-16 14:08:30 +08:00
}
2019-12-16 15:39:29 +08:00
}
})
2019-12-16 17:06:42 +08:00
vh.gameMode.onClick = () => {
2020-01-06 10:43:18 +08:00
popover(context).show(vlayout(
[
...[
{
label: "黑方:人 白方:人",
mode: GameMode.P2P,
},
{
label: "黑方:人 白方:机",
mode: GameMode.P2C,
},
{
label: "黑方:机 白方:人",
mode: GameMode.C2P,
},
].map((e) => text({
text: e.label,
textSize: 20,
textColor: Color.WHITE,
layoutConfig: layoutConfig().just(),
height: 50,
width: 300,
backgroundColor: (state.gameMode === e.mode) ? Color.parse('#636e72') : Color.parse('#b2bec3'),
onClick: () => {
this.updateState(s => {
s.gameMode = e.mode
this.reset(s)
})
popover(context).dismiss()
},
}))
],
{
2019-12-16 17:06:42 +08:00
layoutConfig: layoutConfig().most(),
onClick: () => {
popover(context).dismiss()
},
gravity: Gravity.Center,
2020-01-06 10:43:18 +08:00
})
2019-12-16 17:06:42 +08:00
)
}
2019-12-17 17:29:36 +08:00
vh.result.onClick = () => {
switch (state.gameState) {
case "idle":
this.updateState(state => {
this.reset(state)
})
break
case "blackWin":
case "whiteWin":
break
}
}
vh.currentRole.onClick = () => {
switch (state.gameState) {
case "idle":
break
case "blackWin":
case "whiteWin":
this.updateState(state => {
this.reset(state)
})
break
}
}
2019-12-17 17:50:51 +08:00
vh.assistant.onClick = () => {
const it = this.getState()
if (it.gameState !== 'idle') {
return
}
this.computeNextStep(it)
if (it.gameState !== 'idle') {
return
}
if (it.role === 'black' && it.gameMode === GameMode.C2P) {
setTimeout(() => {
this.computeNextStep(it)
}, 0)
} else if (it.role === 'white' && it.gameMode === GameMode.P2C) {
setTimeout(() => {
this.computeNextStep(it)
}, 0)
}
}
2019-12-16 14:08:30 +08:00
}
2019-12-16 17:28:50 +08:00
computeNextStep(it: GoBangState) {
2019-12-17 15:45:00 +08:00
const tempMatrix: State[] = new Array(count * count).fill(0).map((_, idx) => {
return it.matrix.get(idx) || State.Unspecified
})
let idx = 0
2019-12-16 17:28:50 +08:00
do {
2019-12-17 15:45:00 +08:00
idx = this.computer.compute(tempMatrix, it.role === 'black' ? State.BLACK : State.WHITE)
} while (it.matrix.get(idx) === State.Unspecified)
2019-12-16 17:28:50 +08:00
this.updateState(state => {
2019-12-17 15:45:00 +08:00
state.matrix.set(idx, state.role === 'black' ? State.BLACK : State.WHITE)
2019-12-16 17:28:50 +08:00
state.role = state.role === 'black' ? 'white' : 'black'
2019-12-17 17:29:36 +08:00
if (this.checkResult(idx)) {
modal(context).toast(`恭喜获胜方${it.role === 'white' ? "黑方" : "白方"}`)
it.gameState = it.role === 'white' ? 'blackWin' : 'whiteWin'
}
2019-12-16 17:28:50 +08:00
})
}
2019-12-17 15:45:00 +08:00
2019-12-16 17:28:50 +08:00
reset(it: GoBangState) {
it.matrix.clear()
2019-12-17 17:29:36 +08:00
it.gameState = 'idle'
2019-12-16 17:28:50 +08:00
it.role = "black"
it.anchor = undefined
2019-12-17 16:39:59 +08:00
this.computer = new AIComputer(it.matrix)
2019-12-16 17:28:50 +08:00
if (it.gameMode === GameMode.C2P) {
2019-12-17 16:02:14 +08:00
const idx = Math.floor(Math.random() * count) * count + Math.floor(Math.random() * count)
it.matrix.set(idx, State.BLACK)
it.role = 'white'
2019-12-16 17:28:50 +08:00
}
}
2019-12-16 14:08:30 +08:00
onBind(state: GoBangState, vh: GoBangVH) {
2019-12-16 15:39:29 +08:00
vh.targetZone.forEach((v, idx) => {
const zoneState = state.matrix.get(idx)
switch (zoneState) {
2019-12-16 14:08:30 +08:00
case State.BLACK:
2019-12-16 15:39:29 +08:00
v.also(it => {
it.backgroundColor = Color.BLACK
it.corners = state.gap / 2
it.border = {
color: Color.TRANSPARENT,
width: 0,
}
})
2019-12-16 14:08:30 +08:00
break
case State.WHITE:
2019-12-16 15:39:29 +08:00
v.also(it => {
it.backgroundColor = Color.WHITE
it.corners = state.gap / 2
it.border = {
color: Color.TRANSPARENT,
width: 0,
}
})
break
default:
v.also(it => {
it.backgroundColor = Color.TRANSPARENT
it.corners = 0
it.border = {
color: Color.TRANSPARENT,
width: 0,
}
})
2019-12-16 14:08:30 +08:00
break
}
2019-12-16 15:39:29 +08:00
if (state.anchor === idx) {
v.also(it => {
it.backgroundColor = Color.RED.alpha(0.1)
it.corners = 0
it.border = {
color: Color.RED,
width: 1,
}
})
}
2019-12-16 14:40:10 +08:00
})
2019-12-16 17:06:42 +08:00
vh.gameMode.text = `游戏模式: 黑方 ${state.gameMode === GameMode.C2P ? "机" : "人"} 白方 ${state.gameMode === GameMode.P2C ? "机" : "人"}`
2019-12-17 17:29:36 +08:00
switch (state.gameState) {
case "idle":
vh.result.text = "重新开始"
vh.currentRole.text = `当前: ${(state.role === 'black') ? "黑方" : "白方"}`
break
case "blackWin":
vh.result.text = "黑方获胜"
vh.currentRole.text = "重新开始"
break
case "whiteWin":
vh.result.text = "白方获胜"
vh.currentRole.text = "重新开始"
break
}
2019-12-16 14:08:30 +08:00
}
2019-12-16 16:38:53 +08:00
checkResult(pos: number) {
const matrix = this.getState().matrix
const state = matrix.get(pos)
const y = Math.floor(pos / count)
const x = pos % count
const getState = (x: number, y: number) => matrix.get(y * count + x)
///Horitonzal
{
let left = x
while (left >= 1) {
if (getState(left - 1, y) === state) {
left -= 1
} else {
break
}
}
let right = x
while (right <= count - 2) {
if (getState(right + 1, y) === state) {
right += 1
} else {
break
}
}
if (right - left >= 4) {
return true
}
}
///Vertical
{
let top = y
while (top >= 1) {
if (getState(x, top - 1) === state) {
top -= 1
} else {
break
}
}
let bottom = y
while (bottom <= count - 2) {
if (getState(x, bottom + 1) === state) {
bottom += 1
} else {
break
}
}
if (bottom - top >= 4) {
return true
}
}
///LT-RB
{
let startX = x, startY = y
while (startX >= 1 && startY >= 1) {
if (getState(startX - 1, startY - 1) === state) {
startX -= 1
startY -= 1
} else {
break
}
}
let endX = x, endY = y
while (endX <= count - 2 && endY <= count - 2) {
if (getState(endX + 1, endY + 1) === state) {
endX += 1
endY += 1
} else {
break
}
}
if (endX - startX >= 4) {
return true
}
}
///LB-RT
{
let startX = x, startY = y
while (startX >= 1 && startY <= count + 2) {
if (getState(startX - 1, startY + 1) === state) {
startX -= 1
startY += 1
} else {
break
}
}
let endX = x, endY = y
while (endX <= count - 2 && endY >= 1) {
if (getState(endX + 1, endY - 1) === state) {
endX += 1
endY -= 1
} else {
break
}
}
if (endX - startX >= 4) {
return true
}
}
return false
}
2019-12-16 14:08:30 +08:00
}
@Entry
class Gobang extends VMPanel<GoBangState, GoBangVH> {
getViewModelClass() {
return GoBangVM
}
getState(): GoBangState {
return {
count,
2019-12-16 14:40:10 +08:00
gap: this.getRootView().width / 14,
2019-12-16 14:08:30 +08:00
role: "black",
2019-12-16 17:06:42 +08:00
matrix: new Map,
2019-12-17 16:52:43 +08:00
gameMode: GameMode.P2C,
2019-12-17 17:29:36 +08:00
gameState: "idle"
2019-12-16 14:08:30 +08:00
}
}
getViewHolderClass() {
return GoBangVH
}
}