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> = [] winCount = 0 matrix: Map constructor(matrix: Map) { 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 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{ 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 { 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 } }