685 lines
23 KiB
TypeScript
685 lines
23 KiB
TypeScript
import { Stack, hlayout, Group, Color, stack, layoutConfig, LayoutSpec, vlayout, Text, ViewHolder, ViewModel, VMPanel, scroller, modal, text, gravity, Gravity, View, popover } from "doric";
|
|
import { colors } from "./utils";
|
|
|
|
enum State {
|
|
Unspecified,
|
|
BLACK,
|
|
WHITE,
|
|
}
|
|
|
|
const count = 13
|
|
|
|
|
|
class AIComputer {
|
|
wins: Array<Array<{ x: number, y: number }>> = []
|
|
winCount = 0
|
|
matrix: Map<number, State>
|
|
constructor(matrix: Map<number, State>) {
|
|
this.matrix = matrix
|
|
for (let y = 0; y < count; y++) {
|
|
for (let x = 0; x < count - 4; x++) {
|
|
this.wins.push([])
|
|
for (let k = 0; k < 5; k++) {
|
|
this.wins[this.winCount].push({
|
|
x: x + k,
|
|
y,
|
|
})
|
|
}
|
|
this.winCount++;
|
|
}
|
|
}
|
|
|
|
for (let x = 0; x < count; x++) {
|
|
for (let y = 0; y < count - 4; y++) {
|
|
this.wins.push([])
|
|
for (let k = 0; k < 5; k++) {
|
|
this.wins[this.winCount].push({
|
|
x,
|
|
y: y + k,
|
|
})
|
|
}
|
|
this.winCount++;
|
|
}
|
|
}
|
|
|
|
for (let x = 0; x < count - 4; x++) {
|
|
for (let y = 0; y < count - 4; y++) {
|
|
this.wins.push([])
|
|
for (let k = 0; k < 5; k++) {
|
|
this.wins[this.winCount].push({
|
|
x: x + k,
|
|
y: y + k,
|
|
})
|
|
}
|
|
this.winCount++;
|
|
}
|
|
}
|
|
|
|
for (let x = 0; x < count - 4; x++) {
|
|
for (let y = count - 1; y > 3; y--) {
|
|
this.wins.push([])
|
|
for (let k = 0; k < 5; k++) {
|
|
this.wins[this.winCount].push({
|
|
x: x + k,
|
|
y: y - k,
|
|
})
|
|
}
|
|
this.winCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
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) => {
|
|
let idx = 0
|
|
for (let e of win) {
|
|
switch (this.matrix.get(e.x + e.y * count)) {
|
|
case State.WHITE:
|
|
idx++
|
|
break
|
|
case State.BLACK:
|
|
return 0
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return idx
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
this.wins.forEach((e, winIdx) => {
|
|
if (e.filter(e => (e.x + e.y * count) === idx).length === 0) {
|
|
return
|
|
}
|
|
switch (rivalWins[winIdx]) {
|
|
case 1:
|
|
rivalScore[idx] += 1
|
|
break
|
|
case 2:
|
|
rivalScore[idx] += 10
|
|
break
|
|
case 3:
|
|
rivalScore[idx] += 100
|
|
break
|
|
case 4:
|
|
rivalScore[idx] += 10000
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
|
|
switch (myWins[winIdx]) {
|
|
case 1:
|
|
myScore[idx] += 2
|
|
break
|
|
case 2:
|
|
myScore[idx] += 20
|
|
break
|
|
case 3:
|
|
myScore[idx] += 200
|
|
break
|
|
case 4:
|
|
myScore[idx] += 20000
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
})
|
|
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
|
|
}
|
|
}
|
|
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,
|
|
})
|
|
}
|
|
enum GameMode {
|
|
P2P,
|
|
P2C,
|
|
C2P,
|
|
}
|
|
|
|
interface GoBangState {
|
|
count: number
|
|
gap: number
|
|
role: "white" | "black"
|
|
matrix: Map<number, State>
|
|
anchor?: number
|
|
gameMode: GameMode
|
|
gameState: "blackWin" | "whiteWin" | "idle"
|
|
}
|
|
|
|
class GoBangVH extends ViewHolder {
|
|
root!: Group
|
|
gap = 0
|
|
currentRole!: Text
|
|
result!: Text
|
|
targetZone: View[] = []
|
|
gameMode!: Text
|
|
assistant!: Text
|
|
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(
|
|
vlayout(
|
|
[
|
|
text({
|
|
text: "五子棋",
|
|
layoutConfig: layoutConfig().configWidth(LayoutSpec.MOST),
|
|
textSize: 30,
|
|
textColor: Color.WHITE,
|
|
backgroundColor: colors[0],
|
|
textAlignment: gravity().center(),
|
|
height: 50,
|
|
}),
|
|
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: "游戏模式",
|
|
textSize: 20,
|
|
textColor: Color.WHITE,
|
|
layoutConfig: layoutConfig().most().configHeight(LayoutSpec.JUST),
|
|
height: 50,
|
|
backgroundColor: colors[8],
|
|
}),
|
|
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: "提示",
|
|
textSize: 20,
|
|
textColor: Color.WHITE,
|
|
layoutConfig: layoutConfig().just().configWidth(LayoutSpec.MOST),
|
|
height: 50,
|
|
backgroundColor: colors[3],
|
|
}),
|
|
],
|
|
{
|
|
layoutConfig: layoutConfig().fit(),
|
|
backgroundColor: Color.parse('#ecf0f1'),
|
|
}
|
|
)
|
|
).in(this.root)
|
|
}
|
|
}
|
|
|
|
class GoBangVM extends ViewModel<GoBangState, GoBangVH>{
|
|
computer!: AIComputer
|
|
onAttached(state: GoBangState, vh: GoBangVH) {
|
|
if (!this.computer) {
|
|
this.computer = new AIComputer(state.matrix)
|
|
}
|
|
vh.actualBuild(state)
|
|
vh.targetZone.forEach((e, idx) => {
|
|
e.onClick = () => {
|
|
if (state.gameState !== 'idle') {
|
|
return
|
|
}
|
|
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
|
|
})
|
|
} else {
|
|
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
|
|
if (this.checkResult(idx)) {
|
|
modal(context).toast(`恭喜获胜方${it.role === 'white' ? "黑方" : "白方"}`)
|
|
it.gameState = it.role === 'white' ? 'blackWin' : 'whiteWin'
|
|
} else {
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
vh.gameMode.onClick = () => {
|
|
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()
|
|
},
|
|
}))
|
|
],
|
|
{
|
|
layoutConfig: layoutConfig().most(),
|
|
onClick: () => {
|
|
popover(context).dismiss()
|
|
},
|
|
gravity: Gravity.Center,
|
|
})
|
|
)
|
|
}
|
|
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
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
computeNextStep(it: GoBangState) {
|
|
const tempMatrix: State[] = new Array(count * count).fill(0).map((_, idx) => {
|
|
return it.matrix.get(idx) || State.Unspecified
|
|
})
|
|
let idx = 0
|
|
do {
|
|
idx = this.computer.compute(tempMatrix, it.role === 'black' ? State.BLACK : State.WHITE)
|
|
} while (it.matrix.get(idx) === State.Unspecified)
|
|
this.updateState(state => {
|
|
state.matrix.set(idx, state.role === 'black' ? State.BLACK : State.WHITE)
|
|
state.role = state.role === 'black' ? 'white' : 'black'
|
|
if (this.checkResult(idx)) {
|
|
modal(context).toast(`恭喜获胜方${it.role === 'white' ? "黑方" : "白方"}`)
|
|
it.gameState = it.role === 'white' ? 'blackWin' : 'whiteWin'
|
|
}
|
|
})
|
|
}
|
|
|
|
reset(it: GoBangState) {
|
|
it.matrix.clear()
|
|
it.gameState = 'idle'
|
|
it.role = "black"
|
|
it.anchor = undefined
|
|
this.computer = new AIComputer(it.matrix)
|
|
if (it.gameMode === GameMode.C2P) {
|
|
const idx = Math.floor(Math.random() * count) * count + Math.floor(Math.random() * count)
|
|
it.matrix.set(idx, State.BLACK)
|
|
it.role = 'white'
|
|
}
|
|
}
|
|
onBind(state: GoBangState, vh: GoBangVH) {
|
|
vh.targetZone.forEach((v, idx) => {
|
|
const zoneState = state.matrix.get(idx)
|
|
switch (zoneState) {
|
|
case State.BLACK:
|
|
v.also(it => {
|
|
it.backgroundColor = Color.BLACK
|
|
it.corners = state.gap / 2
|
|
it.border = {
|
|
color: Color.TRANSPARENT,
|
|
width: 0,
|
|
}
|
|
})
|
|
break
|
|
case State.WHITE:
|
|
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,
|
|
}
|
|
})
|
|
break
|
|
}
|
|
if (state.anchor === idx) {
|
|
v.also(it => {
|
|
it.backgroundColor = Color.RED.alpha(0.1)
|
|
it.corners = 0
|
|
it.border = {
|
|
color: Color.RED,
|
|
width: 1,
|
|
}
|
|
})
|
|
}
|
|
})
|
|
vh.gameMode.text = `游戏模式: 黑方 ${state.gameMode === GameMode.C2P ? "机" : "人"} 白方 ${state.gameMode === GameMode.P2C ? "机" : "人"}`
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
@Entry
|
|
class Gobang extends VMPanel<GoBangState, GoBangVH> {
|
|
getViewModelClass() {
|
|
return GoBangVM
|
|
}
|
|
getState(): GoBangState {
|
|
return {
|
|
count,
|
|
gap: this.getRootView().width / 14,
|
|
role: "black",
|
|
matrix: new Map,
|
|
gameMode: GameMode.P2C,
|
|
gameState: "idle"
|
|
}
|
|
}
|
|
getViewHolderClass() {
|
|
return GoBangVH
|
|
}
|
|
} |