← 都来码 / Dolaima← 例程索引go/app/snake.go原文

go/app/snake.go · 19502 字节 · 下载原文

// 贪吃蛇 —— 用 **Go** 写的手机游戏

// Snake -- a mobile game written in **Go**

//

// ✅ 能玩。判据是**逐像素量**出来的:蛇头 248 像素(1 格)、蛇身 496(2 格)、

// ✅ Playable. The criterion was measured **pixel by pixel**: snake head 248 pixels (1 cell), body 496 (2 cells),

//    食物 248(1 格),12 拍定时器后蛇头每拍右移一格、吃到食物后变长、撞墙后重开。

//    food 248 (1 cell); with a 12-tick timer the head moves one cell right per tick, grows after eating, and restarts after hitting a wall.

//

// ◆ 手机那套 UI 怎么来的

// ◆ Where the mobile UI set comes from

//

// 开窗 / 绘图 / 输入 / 定时器的实现是 C 写的(`Lib/shared/src/vmlui.c` → `vmlui.vml`),

// Window opening / drawing / input / timers are implemented in C (`Lib/shared/src/vmlui.c` -> `vmlui.vml`),

// 由 `vmltool.config.xml` 的 `<Language Name="go" Libs="vmlui.vml">` 挂给每个前端。

// attached to every frontend by `<Language Name="go" Libs="vmlui.vml">` in `vmltool.config.xml`.

// 数值的唯一真源是 `Lib/c/vml_ui.h`。

// The single source of truth for the numbers is `Lib/c/vml_ui.h`.

//

// Go 这一路原本**跑不了任何游戏**,四处前端缺陷修完才通(`docs/前端游戏能力评估.md` §4.2):

// The Go path originally **could not run any game**; it only worked after four frontend defects were fixed (see the frontend game capability assessment in `docs/`, section 4.2):

//   · `L0`–`L7` 是**长整数寄存器**的拼法 ⇒ `jge L0` 变成"跳寄存器"、静默不跳(死循环)

//   · `L0`-`L7` is the spelling of the **long-integer registers** => `jge L0` became "jump to a register", silently not jumping (infinite loop)

//   · 数组三处都缺:声明不分配、`a[i] = v` 没有代码生成、读取是 `MOVE R0, R0` 空操作

//   · Arrays were missing in three places: declarations did not allocate, `a[i] = v` had no code generation, and reads were a `MOVE R0, R0` no-op

//   · 栈帧只按"块里顶层语句"估算 ⇒ `for i := 0; …` 的 `i` 没算进去、帧开小了被 push 冲掉

//   · The stack frame was estimated from "top-level statements in the block" only => the `i` of `for i := 0; ...` was not counted, the frame was too small and got clobbered by pushes

//   · **控制语句头部里的复合字面量**:`for i < A[1] {` 的那个 `{` 被当成 `A[1]` 的复合

//   · **Compound literals in a control-statement header**: the `{` of `for i < A[1] {` was taken as the compound

//     字面量、把整个循环体当元素列表吃掉(报错却是 "Expected RBRACE, but got IF")

//     literal of `A[1]` and swallowed the whole loop body as an element list (reporting "Expected RBRACE, but got IF")

//   · **形参只搬了第一个**:调用方把实参压栈,而函数序言只从 R0 取了一个,

//   · **Only the first parameter was moved**: the caller pushes the arguments, but the function prologue took only one from R0,

//     且槽位少了 CALL 自己压入的返回地址那 4 字节 ⇒ 除末参外全是垃圾

//     and the slot offsets were short by the 4 bytes of the return address CALL itself pushes => every parameter but the last was garbage

//   ✅ **2026-10-04 复验:上面这几处都已修好** —— L0 标签:8 个连排 `for` 循环跑出 `t = 16`;数组写读:`A[0]=42 A[1]=7` 读回「42 7」;

//   ✅ **re-checked 2026-10-04: all of the above are fixed** -- L0 labels: 8 back-to-back `for` loops give `t = 16`; array write/read: `A[0]=42 A[1]=7` reads back "42 7";

//     `for i := 0; i < 3; i = i + 1` 的栈帧得 `n = 3`;控制头 `for i < A[0] {` 循环正常跑 2 遍;形参+返回 `addf(3,4) = 7`。

//     the `for i := 0; i < 3; i = i + 1` stack frame gives `n = 3`; a control-header `for i < A[0] {` ran its loop 2 times; params+return `addf(3,4) = 7`.

//

// ◆ 还剩两条**真的**限制(写 Go 游戏时绕开)

// ◆ Two **real** limitations remain (work around them when writing a Go game)

//

//   ① **字符串拼接编出来是空串**:`"标" + "题"` 渲染出来一个字都没有

//   ① **String concatenation compiles to an empty string**: `"a" + "b"` renders nothing at all

//      (`GenerateBinaryOp_StringConcat` 手写的那段汇编有问题 ⇒ 分数用**分数条**画,不拼字符串)。

//      (the hand-written assembly in `GenerateBinaryOp_StringConcat` is broken => the score is drawn as a **score bar**, not concatenated text).

//      ⚠ **2026-10-04 复验仍成立**:`ui_2d_text` 的 trace-draw 里 `"a" + "b"` 画成 `str=""`(同一条 trace 里字面量 `"ab"` 画成 `str="ab"`)。

//      ⚠ **still holds on 2026-10-04**: in the `ui_2d_text` trace-draw `"a" + "b"` draws as `str=""` (a literal `"ab"` in the same trace draws as `str="ab"`).

//   ② **函数名别和标准库撞**:自定义 `func sum(...)` 会被链接到 `lib_util_sum`,

//   ② **Do not let function names collide with the standard library**: a custom `func sum(...)` gets linked to `lib_util_sum`,

//      **静默调错函数**(编得过、结果莫名其妙)⇒ 起名带上自己的前缀。

//      **silently calling the wrong function** (it compiles, the result is inexplicable) => prefix your own names.

//      ⚠ **2026-10-04 复验仍成立**:自定义 `func sum(lo,hi int) int` 调 `sum(1,3)` 得 **33554432**(把名字换成 `mysum` 就得 6)。

//      ⚠ **still holds on 2026-10-04**: a custom `func sum(lo,hi int) int` called as `sum(1,3)` yields **33554432** (renaming it to `mysum` yields 6).

//

// ◆ 写法说明

// ◆ Notes on the style

//

// 状态放模块级数组、辅助函数一律无参 —— 这是修上面那些缺陷时逼出来的形状,

// State lives in module-level arrays and helper functions all take no parameters -- a shape forced by fixing those defects above;

// 缺陷修完之后**参数、返回值、嵌套循环、嵌套 `if` 都已实测可用**(见 `docs/前端游戏能力评估.md` §4.2),

// after the fixes **parameters, return values, nested loops and nested `if` were all measured working** (see the frontend game capability assessment in `docs/`, section 4.2),

// 这里保留原样只是为了少一次全量重测。你要写新游戏可以按自然的 Go 写。

// and this file keeps the old shape only to avoid another full re-test. For a new game you can write natural Go.

//   ⚠ **2026-10-04 复验仍成立**:形参+返回 `addf(3,4) = 7`;三层嵌套循环里嵌 `if` 得 `4`。

//   ⚠ **still holds on 2026-10-04**: params + return `addf(3,4) = 7`; a triple-nested loop with a nested `if` gives `4`.

//

// 另外一个实现选择:段表**不做环形回绕**,每走一步把各节整体后移一位(O(长度) 的搬移,

// Another implementation choice: the segment table **does not wrap around**; each step shifts every segment back by one (an O(length) move,

// 最长 40 节无所谓),换来的是所有下标都不用回绕。

// and 40 segments at most does not matter), which buys indexes that never need wrapping.

//

// ◆ 操作

// ◆ Controls

//

// 方向键转向;SELECT 暂停;回车重开;ESC 或返回箭头退出。

// Arrow keys turn; SELECT pauses; Enter restarts; ESC or the back arrow exits.

// 吃到食物 +10 分并加速;撞墙或撞到自己结束(弹对话框问要不要再来一局)。

// Eating food is +10 points and speeds up; hitting a wall or yourself ends the round (a dialog asks whether to play again).


package main

// 显式引用 UI 库:`import "vmlui"` 会**真的去读** `Lib/go/vmlui/vmlui.go`

// Explicitly reference the UI library: `import "vmlui"` actually reads `Lib/go/vmlui/vmlui.go`

// (GenLib 生成、155 个 `ui_*` 声明),把符号登记进编译器的符号表。

// (generated by GenLib, 155 `ui_*` declarations), registering those symbols in the compiler's symbol table.

//

// ⚠ 删掉这一行就**编不过** —— 判据(实测):

// ⚠ Removing this line **fails to compile** - the measured criterion:

//   vmlcli <本文件>  ⇒  error: 未声明的函数 'ui_win_open'(它属于包 'vmlui'…)

//   vmlcli <this file>  =>  error: undeclared function 'ui_win_open' (it belongs to package 'vmlui' ...)

// 所以它是**强制**的,不是文档级的摆设。

// so it is **mandatory**, not documentation-only.

// ⚠ **2026-10-04 复验仍成立**:/tmp 副本删掉这行后实测 `✘ 编译失败`(本文件报「未声明的函数 'ui_math_rand'」)。

// ⚠ **still holds on 2026-10-04**: a /tmp copy with this line removed measured `✘ compile failed` (this file reports 「undefined function 'ui_math_rand'」).

import "vmlui"

// 状态表(一个数组放全部,避免多个全局数组互相踩)

// State table (everything in one array, avoiding several global arrays clobbering each other)

//   0=head 1=len 2=dx 3=dy 4=fx 5=fy 6=score 7=best

//   8=alive 9=paused 10=stepMs 11=cell 12=ox 13=oy 14=sw 15=sh

//   16+2i=第 i 节 x,17+2i=第 i 节 y(i=0 是头;40 节上限 ⇒ 16..95)

//   16+2i = segment i's x, 17+2i = segment i's y (i=0 is the head; 40 segments max => 16..95)

//   100=候选x 101=候选y 102=格x 103=格y 104=格色 105/106=新方向 107=命中 108=临时

//   100=candidate x 101=candidate y 102=cell x 103=cell y 104=cell color 105/106=new direction 107=hit 108=temporary

//   109=退出标志 110=界面语言(0=中文 1=英文)—— 语言码**不能**存包级标量,见 draw

//   109=exit flag 110=UI language (0=Chinese 1=English) -- the language code **cannot** live in a package-level scalar, see draw

var A [112]int

// 扫一遍身上的每一节,看 (A[100],A[101]) 这格有没有被占;结果写 A[107]

// Scan every segment of the body to see whether the cell (A[100],A[101]) is occupied; the result goes into A[107]

func occupied() {
	A[107] = 0
	i := 0
	for i < A[1] {
		if A[16+i*2] == A[100] && A[17+i*2] == A[101] {
			A[107] = 1
		}
		i = i + 1
	}
}

func placeFood() {
	tries := 0
	A[100] = ui_math_rand(20)
	A[101] = ui_math_rand(18)
	occupied()
	for tries < 300 {
		if A[107] != 0 {
			A[100] = ui_math_rand(20)
			A[101] = ui_math_rand(18)
			occupied()
		}
		if A[107] == 0 {
			A[4] = A[100]
			A[5] = A[101]
			tries = 300
		}
		tries = tries + 1
	}
}

func draw() {
	// 界面语言:**只能从状态表里读**,不能读包级变量。

	// UI language: it **can only be read from the state table**, never from a package-level variable.

	// ⚠ 实测(本前端的缺陷):`var lang int` 这种**包级标量**在函数里赋值后读回来恒 0

	// ⚠ Measured (a defect of this frontend): a **package-level scalar** like `var lang int` always reads back 0 after being assigned inside a function

	//   (`var A [112]int` 这种数组才是好的)⇒ 语言码统一寄在 A[110](该槽没人用),

	//   (an array like `var A [112]int` is fine) => the language code is parked in A[110] (nothing else uses that slot),

	//   开局在 main 里问一次宿主,函数里取个局部量再分支。

	//   asking the host once in main at startup, then branching on a local copy inside functions.

	//   ⚠ **2026-10-04 复验仍成立**:包级 `var gs int`,函数里 `gs = 55; println("gs =", gs)` 打出 `gs = 0`(数组 `A[0]=42` 读回 42)。

	//   ⚠ **still holds on 2026-10-04**: a package-level `var gs int`, assigned `gs = 55` in a function and read with `println("gs =", gs)`, prints `gs = 0` (the array `A[0]=42` reads back 42).

	lang := A[110]
	tScore := "Score"
	tBest := "Best"
	tPaused := "Paused (SELECT)"
	if lang == 0 { tScore = "得分" }
	if lang == 0 { tBest = "最高" }
	if lang == 0 { tPaused = "暂停(SELECT 继续)" }

	ui_2d_clear(-15724520)

	// 棋盘格:扁平序号 0..359,行列内联算出来;每格按 (r+c) 的奇偶决定铺不铺

	// Board cells: flat indexes 0..359, with row/column computed inline; whether to fill a cell depends on the parity of (r+c)

	cellN := 0
	for cellN < 360 {
		if (cellN+cellN/20)-((cellN+cellN/20)/2)*2 == 0 {
			ui_2d_rect(A[12]+(cellN-(cellN/20)*20)*A[11], A[13]+(cellN/20)*A[11], A[11]-1, A[11]-1, -15263713, 1, 0, 2)
		}
		cellN = cellN + 1
	}

	// 食物

	// Food

	ui_2d_rect(A[12]+A[4]*A[11], A[13]+A[5]*A[11], A[11]-1, A[11]-1, -131246, 1, 0, 2)

	// 蛇:i=0 是头

	// Snake: i=0 is the head

	i := 0
	for i < A[1] {
		if i == 0 {
			ui_2d_rect(A[12]+A[16+i*2]*A[11], A[13]+A[17+i*2]*A[11], A[11]-1, A[11]-1, -63488, 1, 0, 2)
		}
		if i != 0 {
			ui_2d_rect(A[12]+A[16+i*2]*A[11], A[13]+A[17+i*2]*A[11], A[11]-1, A[11]-1, -11409298, 1, 0, 2)
		}
		i = i + 1
	}

	ui_2d_text(8, 8, tScore, -6643536, 13, 0)
	ui_2d_rect(58, 11, A[6], 10, -11409298, 1, 0, 0)
	ui_2d_text(A[14]/2, 8, tBest, -6643536, 13, 1)
	ui_2d_rect(A[14]/2+46, 11, A[7], 10, -63488, 1, 0, 0)
	if A[9] != 0 {
		ui_2d_text(A[14]/2, A[15]/2, tPaused, -131246, 16, 1)
	}
	ui_2d_present()
}

func reset() {
	A[1] = 3
	A[0] = 2
	A[16] = 8
	A[17] = 8
	A[18] = 7
	A[19] = 8
	A[20] = 6
	A[21] = 8
	A[2] = 1
	A[3] = 0
	A[6] = 0
	A[8] = 1
	A[9] = 0
	A[10] = 170
	placeFood()
}

func gameOver() {
	lang := A[110]
	tTitle := "Snake"
	tOver := "You crashed. Round over.\nPlay again? (choose \"No\" to quit)"
	if lang == 0 { tTitle = "贪吃蛇" }
	if lang == 0 { tOver = "撞到了,这一局结束。\n再来一局?(选「否」退出)" }
	A[8] = 0
	// 音效:单音 ui_audio_beep;**结局音取最低音**(吃到 1047 / 撞到 131,差得开)

	// Sound: single-tone ui_audio_beep; **the ending tone takes the lowest pitch** (eating 1047 / crashing 131, far enough apart)

	//   v0.96.509 从音序器换回来 —— 那一版多声部叠加 / 长音在真机上破音

	//   switched back from the sequencer in v0.96.509 -- that version's multi-voice layering / long notes crackled on the real device

	ui_audio_beep(131, 320)
	draw()
	// 选「否/拒绝」→ 退出游戏(ui_dlg_msg 返回 0=是 / 1=否)。

	// Choosing "No / reject" -> quit the game (ui_dlg_msg returns 0=yes / 1=no).

	// 此前不接返回值 ⇒ 两个按钮一个样、游戏还退不出去(用户实测报的)。

	// The return value used to be ignored => both buttons behaved the same and the game could not be exited (reported by the user on a real device).

	// 用 A[109] 这个没人用的槽当退出标志 —— gameOver 有四五处调用点,逐个改返回值不划算。

	// A[109], an unused slot, serves as the exit flag -- gameOver has four or five call sites and changing each return value was not worth it.

	if ui_dlg_msg(tTitle, tOver, 0) != 0 {
		A[109] = 1
		return
	}
	reset()
}

func step() {
	if A[8] == 0 || A[9] != 0 {
		return
	}
	nc := A[16] + A[2]
	nr := A[17] + A[3]
	if nc < 0 || nc >= 20 || nr < 0 || nr >= 18 {
		gameOver()
		return
	}
	// 自撞:只看前 len-1 节(尾巴这一步会挪走)

	// Self-collision: only check the first len-1 segments (the tail moves away in this step)

	A[100] = nc
	A[101] = nr
	A[107] = 0
	i := 0
	for i < A[1]-1 {
		if A[16+i*2] == nc && A[17+i*2] == nr {
			A[107] = 1
		}
		i = i + 1
	}
	if A[107] != 0 {
		gameOver()
		return
	}

	// 整体后移一位(不做环形回绕,见文件头「写法说明」)

	// Shift everything back by one (no wrap-around; see "Notes on the style" in the file header)

	i = A[1]
	for i > 0 {
		A[16+i*2] = A[16+(i-1)*2]
		A[17+i*2] = A[17+(i-1)*2]
		i = i - 1
	}
	A[16] = nc
	A[17] = nr

	if nc == A[4] && nr == A[5] {
		if A[1] < 40 {
			A[1] = A[1] + 1
		}
		A[6] = A[6] + 10
		if A[6] > A[7] {
			A[7] = A[6]
		}
		if A[10] > 70 {
			A[10] = A[10] - 6
		}
		// 音效:单音 ui_audio_beep(吃到食物:一声高而短的「叮」)

		// Sound: single-tone ui_audio_beep (eating food: one high, short "ding")

		ui_audio_beep(1047, 165)
		placeFood()
	}
}

func turn() {
	if A[105]+A[2] == 0 && A[106]+A[3] == 0 {
		return
	}
	A[2] = A[105]
	A[3] = A[106]
}

func main() {
	// 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),存进 A[110](见 draw 的说明)。

	// UI language: ask the host once at startup whether Chinese or English (0=Chinese 1=English) and store it in A[110] (see the note in draw).

	// ⚠ 别在每帧里调 —— 那是一次 syscall。

	// ⚠ Do not call it every frame -- that is a syscall.

	A[110] = ui_scr_get_language()
	lang := A[110]
	tTitle := "Snake"
	if lang == 0 { tTitle = "贪吃蛇" }
	// 开窗时传 0:尺寸交给宿主(用真实可用区)。**开窗之后**再问一次真实画布 —— 开窗前问到的只是估算值。

	// Open with 0: the host decides the size (the real usable area). Ask the **real** canvas only after the window is open -- before that the host can only estimate.

	ui_win_open(tTitle, 0, 0)

	w := ui_scr_get_w()
	h := ui_scr_get_h()
	if w <= 0 {
		w = 360
	}
	if h <= 0 {
		h = 620
	}
	A[14] = w
	A[15] = h

	// 版面算一次,画与判定共用

	// The layout is computed once and shared by drawing and hit-testing

	byw := w - 8
	byh := h - 46
	A[11] = byw / 20
	if byh/18 < A[11] {
		A[11] = byh / 18
	}
	if A[11] < 4 {
		A[11] = 4
	}
	A[12] = (w - A[11]*20) / 2
	A[13] = 40

	ui_scr_set_keep_on(1)
	reset()

	tid := ui_timer_set(A[10], 0)
	curMs := A[10]
	acc := 0
	lastMs := ui_time_get_ms()   // 墙钟起点:进主循环时取(不在模块级调 syscall)

	// wall-clock origin: sampled when entering the main loop (no syscall at module level)


	for ui_win_is_closed() == 0 {

		if A[109] != 0 {
			break
		}
		draw()
		t := ui_msg_fetch_wait(0)

		// 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。

		// how many steps this frame takes: **driven by the wall-clock deficit** (timer resolution differs per platform => never treat "ticks received" as time).

		// 用户 2026-10-04 口径:所有游戏时钟取系统毫秒数。

		// user's 2026-10-04 rule: all game clocks read system milliseconds.

		nowMs := ui_time_get_ms()
		acc = acc + (nowMs - lastMs)
		lastMs = nowMs
		if acc > A[10]*8 {
			acc = 0                  // 卡太久别一次补一大串(防瞬移)

			// do not catch up in one huge burst after a long stall (prevents teleporting)

		}
		for acc >= A[10] {
			acc = acc - A[10]
			step()
		}

		if t == 10 {
			break
		}
		if t == 9 {
			// 定时器**只负责唤醒**(推进在上面按墙钟欠账走)

			// the timer **only wakes us up** (advancement happens above, driven by the wall-clock deficit)

			if A[10] != curMs {
				ui_timer_kill(tid)
				curMs = A[10]
				tid = ui_timer_set(curMs, 0)
			}
		}
		if t == 1 {
			k := ui_msg_get_a()
			if k == 27 {
				break
			}
			if k == 37 {
				A[105] = -1
				A[106] = 0
				turn()
			}
			if k == 39 {
				A[105] = 1
				A[106] = 0
				turn()
			}
			if k == 38 {
				A[105] = 0
				A[106] = -1
				turn()
			}
			if k == 40 {
				A[105] = 0
				A[106] = 1
				turn()
			}
			if k == 13 {
				reset()
			}
			if k == 16 {
				A[9] = 1 - A[9]
			}
		}
	}

	ui_timer_kill(tid)
	ui_scr_set_keep_on(0)
	ui_win_close()
}