// 贪吃蛇 —— 用 **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()
}
