kotlin/app/gomoku.kt · 26803 字节 · 下载原文
// 五子棋 —— 用 **Kotlin** 写的手机游戏(15×15、人机对战、悔棋、重开)
// Gomoku — a phone game written in **Kotlin** (15x15, human vs. a simple AI, undo, restart)
//
// 玩法:手指点棋盘落子(黑子,先手),AI 白子应一手。先连成五子者胜。
// Play: tap the board to place your stone (black, first), the AI answers with white. Five in a row wins.
//
// ## 这一份例程的定位:**压 Kotlin 前端的"类外"特性**
// ## What this example is for: **stressing the Kotlin frontend's non-class features**
//
// 写它的时候(2026-09-30)前端刚修掉一批静默错编,这一份刻意把修好的那些用上, 于是它同时是"Kotlin 能写多像 Kotlin"的演示与那份修复的验收:
// Written right after a batch of silent-miscompile fixes (2026-09-30); it deliberately uses what got
// fixed, so it doubles as a demo and as acceptance for that batch:
//
// · **字符串模板**(`"轮到你了(第 $gMoves 手)"`)—— 修之前会静默打出一串数字, 而且中间结果**直接画进别的绘制层**(模板那段发的是 `print_int`)。
// · **string templates** (`"Your turn (move $gMoves)"`) — before the fix these silently printed a run of digits
// and the intermediate results were **drawn into other layers** (the template path emitted `print_int`).
// · **多参数函数**(`fun place(idx: Int, pl: Int)`)—— 修之前形参**整体倒序**。
// · **multi-parameter functions** (`fun place(idx: Int, pl: Int)`) — before the fix the parameters were reversed.
// · **`when` 作表达式** + **无主语 `when { }`**(`patScore`)—— 修之前前者把标签地址当结果。
// · **`when` as an expression** + **subject-less `when { }`** (`patScore`) — the former used to yield a label address.
// · **`x in a..b` 与 `when (x) { in a..b -> }`** —— 修之前后者每个分支都被跳过。
// · **`x in a..b` and `when (x) { in a..b -> }`** — before the fix every `in` branch was skipped.
// · **扩展函数**(`fun Int.row()`)与 **`.size`** —— 都曾被前端拦住或算错。
// · **extension functions** (`fun Int.row()`) and **`.size`** — both used to be blocked or miscalculated.
// · **`if/else` 必须写全**(**C 风格三目 `cond ? a : b` 仍不能用**:2026-10-04 复验仍静默错编成条件值本身;Kotlin 的 Elvis `?:` 已经能用)。
// · **`if/else` must be written out** (the **C-style ternary `cond ? a : b` still cannot be used**: as of 2026-10-04 it still silently compiles to the condition value; Kotlin's Elvis `?:` works now).
//
// ## 两条写法约束(照 `catch.kt` / `demo_ui.kt` 的实测)
// ## Two style constraints (measured, per `catch.kt` / `demo_ui.kt`)
//
// ① **消息只用标量版读**(`ui_msg_fetch_wait` / `ui_msg_get_a` / `ui_msg_get_b`)—— 数组传给库的 `int*` 形参时指针会落偏,Kotlin 侧读到的是错位一格的四个数。
// ① **Read messages with the scalar API only** (`ui_msg_fetch_wait` / `ui_msg_get_a` / `ui_msg_get_b`) — arrays passed to a library's
// `int*` parameter land off by a slot, so the Kotlin side reads a shifted quad.
// ② **状态数组写在文件级 `var`**(本前端的顶层属性已经能用了;`catch.kt` 里那条 "文件级数组读回 0"是老黄历,见 `FRONTEND_DEFECTS.md`)。
// ② **State arrays live in a file-level `var`** (top-level properties work now; the old "file-level array reads back 0"
// note in `catch.kt` is stale — see `FRONTEND_DEFECTS.md`).
//
// 跑法 —— 手机:`vml run examples/kotlin/gomoku.kt`
// How to run — on the phone: `vml run examples/kotlin/gomoku.kt`
// 跑法 —— 桌面:`vmlcli Examples/kotlin/gomoku.kt --screen 411x726 --frames /tmp/fr`
// How to run — on the desktop: `vmlcli Examples/kotlin/gomoku.kt --screen 411x726 --frames /tmp/fr`
// 自动对局(双方都走同一套启发式、跑满 24 手后自己退出并打印棋盘):
// Self-play (both sides run the same heuristic, exits after 24 moves and dumps the board):
// `vmlcli Examples/kotlin/gomoku.kt --screen 411x726 --arg demo`
// ── 显式引用:下面用到的 `ui_*` 来自共享库 vmlui.vml ──────────────────
// ── Explicit reference: the `ui_*` used below come from the shared library vmlui.vml ──
// ⚠ `.*` 必须有:`import vmlui`(无 `.*`)会被 `ExtractImports` 的正则**静默忽略**。
// ⚠ The `.*` is required: `import vmlui` (without `.*`) is silently ignored by `ExtractImports`.
// ⚠ 但整行删掉也照样编、照样跑 —— 前端靠 `ui_` 前缀**自动**链 `vmlui.vml`。
// ⚠ But deleting the whole line still compiles and runs — the frontend auto-links `vmlui.vml` from the `ui_` prefix.
// ⇒ 所以这条是**文档级**的显式声明,不是强制开关(判据见 `FRONTEND_DEFECTS.md` 的 Kotlin 一节)。
// ⇒ So this is a **documentation-level** explicit reference, not a hard switch (see the Kotlin section of `FRONTEND_DEFECTS.md`).
// ✅ 2026-10-04 复验仍成立:三种写法(`import vmlui` / `import vmlui.*` / 整行删掉)产出的指令数完全相同(59377 条)。
// ✅ Re-verified 2026-10-04, still holds: all three forms (`import vmlui` / `import vmlui.*` / line deleted) produce the identical instruction count (59377).
#include "../../../Lib/kotlin/vml_ui.kt"
import vmlui.*
// 棋盘边长(15×15)
// The board side (15x15)
const val N = 15
const val EMPTY = 0
const val HUMAN = 1
const val AI = 2
// ── 状态:颜色写十六进制 `0xAARRGGBB` ──
// ── State; colors are hex `0xAARRGGBB` ──
// 历史:v0.96.592 及之前本前端不认 `0x` 字面量(词法器把它切成 `0` + 一个标识符)。
// History: up to v0.96.592 this frontend did not accept `0x` literals (the lexer split them into `0` plus an identifier).
// 实参位置还会被静默吃掉 ⇒ 颜色变 0、一条错都不报;当时只能用负十进制绕过去。
// In argument position the stray identifier was swallowed silently ⇒ the color became 0 with no error; negative decimals were the workaround.
// 现已修(词法器认 `0x`/`0b`,解析器按 **32 位字**收窄)。
// Fixed now (the lexer accepts `0x`/`0b` and the parser narrows to a **32-bit word**).
// ✅ 2026-10-04 复验仍成立:`0xFF` 得 255、`0xFF101820` 得 -15722464、`0b1011` 得 11,实参位置也不再被吃掉。
// ✅ Re-verified 2026-10-04, still holds: `0xFF` gives 255, `0xFF101820` gives -15722464, `0b1011` gives 11, and argument positions are no longer swallowed.
// 下面那张是当年的对照表,留作记录。
// The table below is the old cross-reference, kept for the record.
// BG 0xFF101018 = -15724520 PANEL 0xFF3A4356 = -12958890
// LINE 0xFF8894AC = -7826260 TITLE 0xFFE8E8F0 = -1513232
// HUMAN 0xFF16161C = -15329764 AI 0xFFF6F6FA = -592134
// GOLD 0xFFFFC040 = -16320 GREEN 0xFF51E86E = -11409298
// MUTED 0xFF9AA0B0 = -6643536 RED 0xFFE06050 = -2072496
//
// ⚠ 棋盘面板特意用**浅色**(石板灰):第一版面板与"黑子"都贴近深色背景,出图一看 黑子几乎看不见 —— 深色棋盘 + 深色棋子 = 没有对比度。这一条**只有真出图才看得出来**。
// ⚠ The board panel is deliberately **light** (slate): in the first render both the panel and the "black"
// stones sat close to the dark background and the black stones were nearly invisible.
// This is the kind of thing **only an actual rendered frame shows**.
var LANG = 0
var SW = 0
var SH = 0
var BX = 0
var BY = 0
var CELL = 0
var gBoard = arrayOf(
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
var gHist = arrayOf(
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
var gHistLen = 0
var gTurn = HUMAN
var gOver = 0
var gWinner = 0
var gMoves = 0
var gLast = -1
// 键盘光标((7,7) 正中那一格)
// The keyboard cursor (the centre cell, (7,7))
var gCursor = 112
var gMaxPrims = 0
var gFrame = 0
var gFrameLimit = 0
var gAuto = 0
// `.size` 与 `#` 下标:数组是"定长"的,长度直接从块头读(前端已修)
// `.size` and `#` indexing: the array is fixed length, read from the block header (fixed in the frontend)
fun cells(): Int { return gBoard.size }
// 扩展函数:`fun Int.row()`。修之前前端**解析不了**接收者是 `Int` 的声明
// Extension functions. Before the fix the frontend **could not parse** a declaration whose receiver is `Int`.
fun Int.row(): Int { return this / N }
fun Int.col(): Int { return this % N }
/// 四连判定:从 `idx` 出发沿 (dx,dy) 一个方向,数出连续的 `pl` 有几颗(含自己)
/// Run length: starting at `idx`, count consecutive `pl` stones along (dx,dy), including the cell itself.
fun dirRun(idx: Int, dx: Int, dy: Int, pl: Int): Int {
var r = idx.row()
var c = idx.col()
var cnt = 1
var steps = 0
var rr = r + dx
var cc = c + dy
while (steps < 4 && rr >= 0 && rr < N && cc >= 0 && cc < N && gBoard[rr * N + cc] == pl) {
cnt = cnt + 1
rr = rr + dx
cc = cc + dy
steps = steps + 1
}
steps = 0
rr = r - dx
cc = c - dy
while (steps < 4 && rr >= 0 && rr < N && cc >= 0 && cc < N && gBoard[rr * N + cc] == pl) {
cnt = cnt + 1
rr = rr - dx
cc = cc - dy
steps = steps + 1
}
return cnt
}
fun winsAt(idx: Int, pl: Int): Int {
if (dirRun(idx, 1, 0, pl) >= 5) { return 1 }
if (dirRun(idx, 0, 1, pl) >= 5) { return 1 }
if (dirRun(idx, 1, 1, pl) >= 5) { return 1 }
if (dirRun(idx, 1, -1, pl) >= 5) { return 1 }
return 0
}
/// 形状打分 —— **无主语 `when` 作表达式**(修之前它把数据段标签的地址当结果返回)
/// Shape scoring — a **subject-less `when` as an expression** (it used to return the address of a data label).
fun patScore(cnt: Int, open: Int): Int {
return when {
cnt >= 5 -> 1000000
cnt == 4 && open == 2 -> 120000
cnt == 4 && open == 1 -> 12000
cnt == 3 && open == 2 -> 9000
cnt == 3 && open == 1 -> 600
cnt == 2 && open == 2 -> 400
cnt == 2 && open == 1 -> 40
cnt == 1 && open == 2 -> 20
else -> 1
}
}
/// 假想 `idx` 放一颗 `pl`,这个形状值多少分(把"假设的那颗"也算进连子)
/// Score of pretending `idx` holds a `pl` stone (the hypothetical stone counts toward the run).
fun dirScore(idx: Int, dx: Int, dy: Int, pl: Int): Int {
var r = idx.row()
var c = idx.col()
var cnt = 1
var open = 0
var steps = 0
var rr = r + dx
var cc = c + dy
while (steps < 4 && rr >= 0 && rr < N && cc >= 0 && cc < N && gBoard[rr * N + cc] == pl) {
cnt = cnt + 1
rr = rr + dx
cc = cc + dy
steps = steps + 1
}
if (rr >= 0 && rr < N && cc >= 0 && cc < N && gBoard[rr * N + cc] == EMPTY) { open = open + 1 }
steps = 0
rr = r - dx
cc = c - dy
while (steps < 4 && rr >= 0 && rr < N && cc >= 0 && cc < N && gBoard[rr * N + cc] == pl) {
cnt = cnt + 1
rr = rr - dx
cc = cc - dy
steps = steps + 1
}
if (rr >= 0 && rr < N && cc >= 0 && cc < N && gBoard[rr * N + cc] == EMPTY) { open = open + 1 }
return patScore(cnt, open)
}
fun cellScore(idx: Int, pl: Int): Int {
return dirScore(idx, 1, 0, pl) + dirScore(idx, 0, 1, pl)
+ dirScore(idx, 1, 1, pl) + dirScore(idx, 1, -1, pl)
}
/// 启发式:给每个空点算"我方价值 ×11 + 对方价值 ×10",取最高。 / 系数只差一点 ⇒ 同样分量的进攻比防守优先一点点,但对方成四时防守分压倒一切(能挡)。
/// Heuristic: for every empty point take `my value * 11 + their value * 10`, keep the max.
/// The slight edge to attack only breaks ties; a four for the opponent still dominates (so it blocks).
fun bestMove(pl: Int, opp: Int): Int {
var best = -1
var bestScore = -1
var i = 0
while (i < N * N) {
if (gBoard[i] == EMPTY) {
var s = cellScore(i, pl) * 11 + cellScore(i, opp) * 10
if (s > bestScore) { bestScore = s; best = i }
}
i = i + 1
}
return best
}
/// 落子。返回 0 = 正常 / 1 = 连成五子 / 2 = 棋盘满了(平局)
/// Place a stone. 0 = ok, 1 = five in a row, 2 = the board is full (draw).
fun place(idx: Int, pl: Int): Int {
if (idx < 0 || idx >= N * N) { return 3 }
if (gBoard[idx] != EMPTY) { return 3 }
gBoard[idx] = pl
gHist[gHistLen] = idx
gHistLen = gHistLen + 1
gLast = idx
gMoves = gMoves + 1
if (winsAt(idx, pl) == 1) { gOver = 1; gWinner = pl; return 1 }
if (gMoves >= N * N) { gOver = 1; gWinner = 0; return 2 }
return 0
}
fun undo() {
var k = 0
while (k < 2 && gHistLen > 0) {
gHistLen = gHistLen - 1
gBoard[gHist[gHistLen]] = EMPTY
gMoves = gMoves - 1
k = k + 1
}
gOver = 0
gWinner = 0
gTurn = HUMAN
if (gHistLen > 0) { gLast = gHist[gHistLen - 1] } else { gLast = -1 }
}
fun restart() {
var i = 0
while (i < N * N) { gBoard[i] = EMPTY; i = i + 1 }
gHistLen = 0
gTurn = HUMAN
gOver = 0
gWinner = 0
gMoves = 0
gLast = -1
gCursor = 112
}
fun relayout() {
SW = ui_scr_get_w()
SH = ui_scr_get_h()
if (SW <= 0) { SW = 411 }
if (SH <= 0) { SH = 726 }
CELL = (SW - 28) / N
BX = (SW - (N - 1) * CELL) / 2
BY = 86
}
fun statusText() {
if (gOver == 1 && gWinner == HUMAN) {
if (LANG == 0) { ui_2d_text(SW / 2, 58, "你赢了!", 0xFF51E86E, 15, VML_ANCHOR_CENTER) }
else { ui_2d_text(SW / 2, 58, "You win!", 0xFF51E86E, 15, VML_ANCHOR_CENTER) }
} else if (gOver == 1 && gWinner == AI) {
if (LANG == 0) { ui_2d_text(SW / 2, 58, "AI 赢了", 0xFFE07050, 15, VML_ANCHOR_CENTER) }
else { ui_2d_text(SW / 2, 58, "AI wins", 0xFFE07050, 15, VML_ANCHOR_CENTER) }
} else if (gOver == 1) {
if (LANG == 0) { ui_2d_text(SW / 2, 58, "平局(棋盘满了)", 0xFF9AA0B0, 15, VML_ANCHOR_CENTER) }
else { ui_2d_text(SW / 2, 58, "Draw (board full)", 0xFF9AA0B0, 15, VML_ANCHOR_CENTER) }
} else if (gTurn == HUMAN) {
// **字符串模板**:修之前这一句会静默打出一串数字,而不是画在状态行上
// **String template**: before the fix this silently printed digits instead of drawing on the status line
// ✅ 2026-10-04 复验:字符串模板已可用(`"n=$n"` ⇒ `n=7`),这一段保持原样。
// ✅ Re-verified 2026-10-04: string templates work (`"n=$n"` => `n=7`); this section is unchanged.
if (LANG == 0) { ui_2d_text(SW / 2, 58, "轮到你了(第 $gMoves 手)", 0xFFE8E8F0, 15, VML_ANCHOR_CENTER) }
else { ui_2d_text(SW / 2, 58, "Your turn (move $gMoves)", 0xFFE8E8F0, 15, VML_ANCHOR_CENTER) }
} else {
if (LANG == 0) { ui_2d_text(SW / 2, 58, "AI 在想…", 0xFF9AA0B0, 15, VML_ANCHOR_CENTER) }
else { ui_2d_text(SW / 2, 58, "AI thinking…", 0xFF9AA0B0, 15, VML_ANCHOR_CENTER) }
}
}
fun draw() {
ui_2d_clear(0xFF101018)
// 标题
// title
if (LANG == 0) { ui_2d_text_styled(SW / 2, 18, "五子棋", 0xFFE8E8F0, 20, VML_ANCHOR_CENTER, VML_FONT_BOLD) }
else { ui_2d_text_styled(SW / 2, 18, "Gomoku", 0xFFE8E8F0, 20, VML_ANCHOR_CENTER, VML_FONT_BOLD) }
statusText()
// 棋盘底 + 网格(15 横 + 15 竖 = 30 条线)
// The board panel plus the grid (15 rows + 15 columns = 30 lines)
ui_2d_rect(BX - 8, BY - 8, (N - 1) * CELL + 16, (N - 1) * CELL + 16, 0xFF3A4356, 1, 0, 8)
var i = 0
while (i < N) {
ui_2d_line(BX, BY + i * CELL, BX + (N - 1) * CELL, BY + i * CELL, 0xFF8894AC, 1)
ui_2d_line(BX + i * CELL, BY, BX + i * CELL, BY + (N - 1) * CELL, 0xFF8894AC, 1)
i = i + 1
}
// 五个星位
// five star points
ui_2d_circle(BX + 3 * CELL, BY + 3 * CELL, 3, 0xFF8894AC, 1, 0)
ui_2d_circle(BX + 11 * CELL, BY + 3 * CELL, 3, 0xFF8894AC, 1, 0)
ui_2d_circle(BX + 3 * CELL, BY + 11 * CELL, 3, 0xFF8894AC, 1, 0)
ui_2d_circle(BX + 11 * CELL, BY + 11 * CELL, 3, 0xFF8894AC, 1, 0)
ui_2d_circle(BX + 7 * CELL, BY + 7 * CELL, 3, 0xFF8894AC, 1, 0)
// 棋子(**上界就是 225 颗**,见文件末尾的图元数说明)
// Stones; the bound is 225 (see the primitive-count note at the end of the file)
i = 0
while (i < N * N) {
var v = gBoard[i]
if (v != EMPTY) {
var px = BX + i.col() * CELL
var py = BY + i.row() * CELL
if (v == HUMAN) { ui_2d_circle(px, py, CELL / 2 - 2, 0xFF16161C, 1, 0) }
else { ui_2d_circle(px, py, CELL / 2 - 2, 0xFFF6F6FA, 1, 0) }
}
i = i + 1
}
// 最后一手 + 键盘光标
// last move + keyboard cursor
if (gLast >= 0) {
ui_2d_rect(BX + gLast.col() * CELL - 4, BY + gLast.row() * CELL - 4, 8, 8, 0xFFFFC040, 0, 2, 0)
}
if (gCursor >= 0 && gBoard[gCursor] == EMPTY && gOver == 0) {
ui_2d_rect(BX + gCursor.col() * CELL - CELL / 2 + 1, BY + gCursor.row() * CELL - CELL / 2 + 1,
CELL - 2, CELL - 2, 0xFF51E86E, 0, 1, 3)
}
// 两个按钮
// two buttons
var bw = (SW - 36) / 2
var byy = BY + (N - 1) * CELL + 28
ui_2d_rect(12, byy, bw, 42, 0xFF3A4356, 1, 0, 8)
ui_2d_rect(24 + bw, byy, bw, 42, 0xFF3A4356, 1, 0, 8)
if (LANG == 0) { ui_2d_text(12 + bw / 2, byy + 21, "悔棋", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }
else { ui_2d_text(12 + bw / 2, byy + 21, "Undo", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }
if (LANG == 0) { ui_2d_text(24 + bw + bw / 2, byy + 21, "重开", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }
else { ui_2d_text(24 + bw + bw / 2, byy + 21, "Restart", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER) }
// 底部提示
// bottom hint
// ⚠ 英文比中文长:第一版那句 66 个字符、12 号字排出来比 411 还宽,**两边都被裁掉** (出图才看出来)。这行按"一个字大约 6 像素"估长度,宁可短一点。
// ⚠ English runs longer than Chinese: the first version's 66-character line was wider than 411 at
// size 12 and got **clipped on both sides** (only the rendered frame showed it). Budget ~6 px per
// character here and keep it short.
if (LANG == 0) { ui_2d_text(SW / 2, byy + 62, "点棋盘落子 · 方向键+回车也行 · U 悔棋 · R 重开 · Esc 退出", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) }
else { ui_2d_text(SW / 2, byy + 62, "Tap or arrows+Enter · U undo · R restart · Esc quit", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) }
// 图元数自检:**量的是"这一帧往场景里放了多少条"**(`ui_2d_present` 之前的那个数)
// Primitive self-check: the count of scene primitives **before `ui_2d_present` flushes them**
var n = ui_res_get_count(0)
if (n > gMaxPrims) { gMaxPrims = n }
ui_2d_present()
}
/// 落子 + AI 应手(一拍走完,AI 是纯启发式、无随机 ⇒ 可复现)
/// Place + the AI's reply (one tick; the AI is a pure heuristic with no randomness, so it is reproducible)
fun humanThenAi(idx: Int) {
if (gOver == 1) { return }
if (gBoard[idx] != EMPTY) { ui_audio_beep(300, 60); return }
var rc = place(idx, HUMAN)
if (rc == 1) { ui_audio_beep(1047, 300); return }
ui_audio_beep(880, 60)
gTurn = AI
var ai = bestMove(AI, HUMAN)
var rc2 = place(ai, AI)
if (rc2 == 1) { ui_audio_beep(220, 420); return }
if (rc2 == 2) { ui_audio_beep(330, 300); return }
gTurn = HUMAN
ui_audio_beep(520, 60)
}
fun dumpBoard() {
var i = 0
while (i < N * N) {
printf("%d", gBoard[i])
if (i.col() == N - 1) { printf("\n") }
i = i + 1
}
}
fun main() {
var w = 0
var h = 0
LANG = ui_scr_get_language()
w = ui_scr_get_w()
h = ui_scr_get_h()
if (w <= 0) { w = 411 }
if (h <= 0) { h = 726 }
// 竖屏 + 不要手柄区(画布吃满整屏)/ portrait + no gamepad area (the canvas fills the screen) 标题也双语
// the title is bilingual too
// ⚠ 上面那两句是**开窗前的估算值**,只作兜底;开窗后 `relayout()` 会重新问一次拿到**实测画布**。
// ⚠ The two reads above are the **pre-open estimate**, a fallback only; `relayout()` re-reads the **measured canvas** after the open.
// 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)。
// Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate).
if (LANG == 0) { ui_win_open_ex("五子棋", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD) } else { ui_win_open_ex("Gomoku", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD) }
ui_scr_lock_orientation(VML_LOCK_PORTRAIT) // VML_LOCK_PORTRAIT
ui_scr_set_keep_on(1)
// 无头验收用的两档"自对局"(不传参数时是完全正常的对局,跑多久都行): `--arg demo` → 双方都走同一套启发式,跑满 24 手自己退出并打印棋盘 `--arg demo --arg N` → 同上但跑满 220 手 —— **用来量"满盘时每帧图元数"的上界** 判据只用 `ui_arg_get_count()`(读参数值要传缓冲区,而 `ui_arg_get` 收数组,Kotlin 侧那条约已知会错位)
// Two headless self-play modes (with no argument the game is completely normal and runs freely):
// `--arg demo` → both sides run the same heuristic, exits after 24 moves and dumps the board
// `--arg demo --arg N` → same but 220 moves — **used to measure the per-frame primitive bound on a full board**
// The test only uses `ui_arg_get_count()` (reading argument *values* needs a buffer, and `ui_arg_get` takes an array,
// which is the known-shifted path on the Kotlin side)
if (ui_arg_get_count() > 1) {
gAuto = 1
gFrameLimit = 24
if (ui_arg_get_count() > 2) { gFrameLimit = 220 }
}
relayout()
restart()
draw()
var tid = ui_timer_set(60, 1)
while (ui_win_is_closed() == 0) {
var t = ui_msg_fetch_wait(120)
if (t == 10) { break } // MSG_WINDOWCLOSE
if (t == 9) { // MSG_TIMER
gFrame = gFrame + 1
if (gFrameLimit > 0 && gFrame <= gFrameLimit && gOver == 0 && gAuto == 1) {
if (gTurn == HUMAN) { humanThenAi(bestMove(HUMAN, AI)) }
}
draw()
if (gFrameLimit > 0 && gFrame >= gFrameLimit) { break }
continue
}
if (t == 1) { // MSG_KEYDOWN
var k = ui_msg_get_a()
// ESC 退出
// ESC quits
if (k == 27) { break }
if (k == 37) { gCursor = gCursor - 1 }
if (k == 39) { gCursor = gCursor + 1 }
if (k == 38) { gCursor = gCursor - N }
if (k == 40) { gCursor = gCursor + N }
if (gCursor < 0) { gCursor = gCursor + N * N }
if (gCursor >= N * N) { gCursor = gCursor - N * N }
if (k == 13 || k == 32 || k == 65) { humanThenAi(gCursor) }
if (k == 85) { undo(); ui_audio_beep(440, 70) } // 'U'
if (k == 82) { restart(); ui_audio_beep(660, 90) } // 'R'
draw()
continue
}
if (t == 6 || t == 4) { // MSG_TOUCHDOWN / MSG_MOUSEDOWN
var x = ui_msg_get_a()
var y = ui_msg_get_b()
var byy = BY + (N - 1) * CELL + 28
var bw = (SW - 36) / 2
if (y >= byy && y <= byy + 42 && x >= 12 && x <= 12 + bw) {
undo()
ui_audio_beep(440, 70)
} else if (y >= byy && y <= byy + 42 && x >= 24 + bw && x <= 24 + bw * 2) {
restart()
ui_audio_beep(660, 90)
} else {
var c = (x - BX + CELL / 2) / CELL
var r = (y - BY + CELL / 2) / CELL
// `in` 判断:区间两端都要查(前端已修 `x in arrayOf(…)` 恒为真那条)
// `in` range check on both ends (the old "`x in arrayOf(...)` is always true" bug is fixed)
if (r in 0..(N - 1) && c in 0..(N - 1)) { humanThenAi(r * N + c) }
}
draw()
continue
}
if (t == 11) { // MSG_WINDOWRESIZE
relayout()
draw()
continue
}
if (t == 12) { continue } // MSG_WINDOWORIENT
}
ui_timer_kill(tid)
ui_scr_set_keep_on(0)
ui_win_close()
// 无头验收用的确定性判据(画面只能肉眼看,这几个数能自动比)
// Deterministic checkpoints for headless verification (graphics need eyes; these numbers compare)
dumpBoard()
printf("gomoku: cells=%d\n", cells())
printf("gomoku: moves=%d over=%d winner=%d last=%d\n", gMoves, gOver, gWinner, gLast)
printf("gomoku: maxprims=%d\n", gMaxPrims)
if (LANG == 0) { printf("gomoku: done\n") } else { printf("gomoku: done\n") }
}