不用一张图片的像素小游戏:小人是字符串,房间是矩形,平衡靠两千个假玩家

说明:这个小游戏是和 AI 编程助手协作开发的,本文也由 AI 协助整理成文。文中代码摘自项目源码(注释改成了中文,有删节),数字来自源码和单元测试的输出。游戏内容纯属虚构,和任何 AI 公司无关;文中没有、游戏里也没有任何"防封技巧"。

前言

《AI 封号模拟器》是一个安卓小游戏:每天一张事件卡,两个选项,赶在"自动审核"把你送走之前把项目做完。前两篇讲了它的规则和怎么给它加玩法。

这一篇讲最近一次大更新里的三件事:

  1. 给游戏加上像素小人、小房间和成就徽章,安装包里没有多一张图片。
  2. 一天之内加了盲盒、副本、上课、人物关系四套东西,胜率没有失控。
  3. 从两种语言加到四种,卡片文件一个字没改。

一、小人是 16 行字符串

没有美术,也不想去找素材(版权说不清)。办法是把像素画直接写成字符串,一个字母一个像素:

kotlin 复制代码
// 一张脸在不同心情下的样子:只画第 5 行以下有变化的部分。'.' 表示这一格不画
Mood.UNEASY to listOf(
    "............b...",   // b:一滴汗
    ".....e....e..b..",   // e:眼睛
    "................",
    ".......mm.......",   // m:嘴,拿不准的那种
),

一个人不是一张图,而是拼出来的:一个头,加一种发型、一件上衣、一张当下的脸,一层层盖上去。

kotlin 复制代码
fun person(look: Look, mood: Mood, blink: Boolean, bob: Boolean): List<String> {
    val rows = Array(SIZE) { BLANK.toCharArray() }
    fun lay(over: List<String>, from: Int) {            // 把一层盖到画上,'.' 的地方透过去
        over.forEachIndexed { y, row -> row.forEachIndexed { x, c -> if (c != '.') rows[from + y][x] = c } }
    }
    lay(head, 0)
    lay(faces.getValue(mood), 5)
    lay(hairs[look.hair], 0)
    lay(look.extra, 0)                                   // 眼镜、黑眼圈
    lay(tops[look.top], 11)
    return rows.map { String(it) }
}

13 个人、6 种表情,如果一张张画是 78 张图;实际只写了 8 种发型、8 种上衣、6 张脸。加一个新人物就是加一行:

kotlin 复制代码
"mom" to Look(HAIR_LONG, 0xFF4A3328, TOP_TEE, 0xFFE58FB0),

几个顺手得到的好处:

  • 改起来像改文字。第一版的眼镜是一条黑杠,看着像打了码,改成灰色只动了一个字母。
  • 能测。有一项测试检查每张画都是 16×16、每个字母都有颜色、任何两个人都不长得一样。
  • 图标和游戏里是同一份数据。测试把图标上那个小人导出成"坐标 + 颜色",一个脚本读它生成启动器图标和商店图标。改了小人,图标不会忘了改。

被封的样子没有画"难过"。16×16 的脸上,嘴角往下撇两个像素几乎看不出来。最后的做法是在眼睛上盖一条红杠,这条红杠后来成了图标。

二、房间是一个个矩形

小人后来有了一个房间:24×16 个像素,窗、架子、台灯、桌子、屏幕。房间也没有图,是代码里一个矩形一个矩形涂上去的:

kotlin 复制代码
val lit = view.sanity >= 40                               // 心态还够,灯就亮着
val rows = List(HEIGHT) { LongArray(WIDTH) { if (lit) WALL else WALL_DIM } }
fun box(x: Int, y: Int, w: Int, h: Int, color: Long) { /* 把这一块涂成这个颜色 */ }

box(1, 1, 6, 6, FRAME)                                    // 窗框
box(2, 2, 4, 4, if (view.night) NIGHT_SKY else DAY_SKY)   // 答完今天的卡,天就黑了
if (view.money >= 55) box(18, 2, 3, 2, GREEN)             // 钱够,架子上有一盆绿植
box(20, 9, 3, 2, when {                                    // 屏幕的颜色就是局势
    view.banned -> RED
    view.nearlyDone -> GREEN
    else -> BLUE
})

房间里的每样东西都对应一个数值:昼夜对应"今天答没答",台灯对应心态,架子对应钱包,屏幕对应账号。玩家不用读数字,瞄一眼房间就知道自己什么处境。原来屏幕上只有三条进度条。

三、加玩法之前,先写"这不是捷径"

这次一天里加了四套东西。每一套都可能让游戏变得太容易,所以每一套都先写一条测试,再调数值。

测试的写法很土:写几种"假玩家",让它们各玩两千局,数赢了多少。

kotlin 复制代码
val plain = (0 until 2_000).count { play(it, careful = true).state.ending in shipped }
val boxing = (0 until 2_000).count { play(it, careful = true, boxes = true).state.ending in shipped }
assertTrue("盲盒最多帮一点忙", boxing < plain + 240 && boxing < 1_800)
assertTrue("也不是坑", boxing > plain - 240)

careful = true 是一个会权衡的假玩家;boxes = true 是在它身上加一个毛病:钱包里有闲钱就去开盲盒。

跑出来的数(两千局里赢的局数):

假玩家 赢 胜率
认真玩 1265 63%
认真玩,爱开盲盒 1360 68%
认真玩,肯上课、接好工作 1447 72%
认真玩,谁肯帮忙就找谁 1273 64%
随便乱选 450 23%

第一版的数值几乎一定偏强。盲盒最初卖 9 块,爱开盲盒的假玩家胜率到了 74%,测试没过,涨到 12 块;课最初 6 块一堂,肯上课的到了 75%,涨到 9 块起。如果没有这条测试,这两个数只能凭感觉。

还有一条测试专门防"写了但永远出不来的卡":每种身份、每个副本各玩一千五百局,96 张卡每一张都必须出现过。

一个小坑:要比较"有某件道具"和"没有"的差别,得让两局的运气完全一样------同一个随机种子开两局。但两边消耗的随机数个数也必须一样,否则后面的牌就对不上了。我第一次让一边去打工(要随机抽一份零活)、另一边去上课(不抽),第二天两边看到的就不是同一张卡。

四、多语言:原句当钥匙

游戏文字原来在数据里是 {zh, en} 两个字段。加第三种语言时没有再加字段,而是换了个思路:中英照旧,其他语言各一张"简体原句 → 译文"的表。

kotlin 复制代码
fun get(language: String, ai: String): String = when (language) {
    CHINESE -> zh
    ENGLISH -> en
    else -> Translations.of(language)[zh] ?: if (language.startsWith(CHINESE)) zh else en
}.replace("{ai}", ai)

加一种语言 = 加一个表 + 一份界面文字,卡片文件和代码都不动。代价是原句改一个字,译文就对不上,所以有一条测试把游戏里能出现的每一句话过一遍:哪种语言漏了哪句、多了哪句没人用的旧话、占位符少了一个,都会报出来。

繁体中文是用 OpenCC 在电脑上转好再放进安装包的,用的是台湾的说法(软件→軟體,项目→專案),手机上不带转换库。日语是逐句翻的。

译文比原文长,会把界面撑破:日语的"プロジェクト"比"项目进度"宽,进度条左边那一栏被截掉了一个字,最后改成了"進捗"。每加一种语言,至少把主要页面截图看一遍。

五、给截图留一个后门

商店截图要拍"被封了""开出了最稀有的道具"这种画面,靠在模拟器里一路点过去,既慢又碰运气。于是在调试版里留了一个入口:启动时带一个参数,直接摆出准备好的局面。

复制代码
adb shell am start -n <包名>/.MainActivity --es pose banned

发布版不认这个参数。四种语言、每种十一个画面,一个脚本几分钟拍完,文案改了就重拍。

小结

  • 像素画写成字符串、房间写成矩形,好处不只是省了美术:它们能被测试、能被脚本读、改起来像改文字。
  • 加系统之前先写"不是捷径"的测试,数值才有地方落脚。
  • 多语言用原句当钥匙,再用一条测试守住"别漏翻"。

游戏完全离线,不要任何权限,不收集任何数据。它还在 Google Play 封闭测试里,个人开发者的新账号要凑够 12 人连续测 14 天才能发正式版。有兴趣的朋友可以这样参加:

  1. 加入测试群组(点"加入群组"就行,不用等审批):https://groups.google.com/g/ai-ban-simulator-testers
  2. 打开测试链接,点"成为测试人员":https://play.google.com/apps/testing/com.weijiazhao.bansim
  3. 用同一个账号在 Play 里安装,装着别卸载,留 14 天
相关推荐
stevenzqzq2 小时前
android 对渠道的理解
android
QEasycloud2 小时前
BOM接口抛转失败钉钉通知策略实战教程:MySQL异常日志到钉钉消息的端到端配置
android·mysql·钉钉
AIsoft_86892 小时前
医患交流内容如何做好记录?
android·iphone
蒸鱼Yuzheng2 小时前
Android Native 崩溃证据链:Tombstone、Build ID 与 Trace 如何联合归因
android·ndk·tombstone·调试符号·崩溃分析
事圆则缓3 小时前
Kotlin 变量与函数:把页面状态和行为写成清楚的契约
android·开发语言·kotlin
hunterandroid3 小时前
MVVM 迁移到 MVI:复杂页面状态如何收敛
android
妖星3 小时前
Android 开机动画定制:从 bootanimation.zip 到源码编译
android
临期冰淇淋4 小时前
MTK平台 安卓16 GMS报错修改
android·java·前端
mmsx4 小时前
Android 模块化三件套:轻量组件化、宿主插件双形态、AIDL 隔离
android·网络·组件化·aidl