博客

  • 网格摆方 1v1

    产品需求文档 (PRD)

    项目名称:《网格摆方 1v1》iOS 移动游戏 MVP

    文档版本: V1.0

    发布日期: 2026年05月31日

    作者: 产品团队

    项目状态: 评审中

    1. 变更履历 (Change Log)

    版本日期作者变更内容说明状态
    V1.02026-05-31产品团队初始版本发布。整合河南传统《摆方》核心规则(成方吃1、成洲吃2、不计对角线、退步不吃子),确立基于 Apple GameKit 框架的去中心化 P2P 网络架构与 Modern Flat 2.0 扁平化视觉风格。评审中

    2. 项目概述与市场定位 (Product Overview)

    2.1 产品背景

    《摆方》(亦称下方、走方、成方)是中国中原地区(尤其是河南农村)流传极广的传统民间双人对弈棋类游戏。其原本依赖于地表划线与天然碎石作为棋具,具备极高的策略深度与极简的博弈框架。本项目旨在通过 iOS 平台将这一非物质文化遗产数字化,利用现代技术手段还原其核心策略魅力。

    2.2 产品定位

    一款基于 iOS 生态、深度绑定 Apple Game Center (GameKit) 的 1v1 在线即时策略对弈手游。主打“零服务器成本、极致原生、高算力对抗、快节奏碎通关”。

    2.3 核心设计哲学

    • 硬核还原: 绝不添加破坏平衡性的道具或数值系统,完全保留民间原汁原味的博弈规则。
    • 极致扁平: 视觉上摒弃传统的泥地、木纹等拟真冗余纹理,采用 Modern Flat 2.0 极简美学,突出棋盘和棋子本身的几何美感与动态交互。
    • 无缝生态: 完全集成 iOS Game Center 体系,免去用户注册、登录的繁琐流程,提供开箱即用的对战体验。

    3. 核心游戏规则与算法逻辑 (Core Game Logic)

    本游戏运行于一个高度确定性的状态机中。任何界面的渲染和数据的网络传输都必须基于以下底层数学规则。

    3.1 棋盘坐标系与棋子配置

    • 棋盘定义: 标准 $5 \times 5$ 的二维正方形网格,共 25 个交叉点。
    • 数学坐标: 建立直角坐标系,左上角点记为 $(0,0)$,右下角点记为 $(4,4)$。全盘共 25 个一维索引编码(0 至 24),计算公式为:$\text{Index} = x + y \times 5$。
    • 兵力总额: 游戏开始时,对局双方(黑方与白方,或在 UI 中表现为深蓝方与亮橙方)各持有 12 枚棋子,全盘总计 24 枚棋子。

    3.2 游戏生命周期:双阶段对局机制

    阶段一:落子阶段(摆方)

    • 交互逻辑: 双方通过 GameKit 随机决定的先后手顺序,轮流点击棋盘上的任意空白交叉点放置一枚己方棋子。
    • 结算检测: 每次成功落子后,系统立即运行“阵型扫描器”(详见3.3节)。若触发“成方”或“成洲”,则暂停常规轮转,进入“吃子交互状态”。
    • 阶段终止条件: 当全盘 25 个交叉点已被填满 24 个(即双方手中的 12 枚棋子全部落盘,棋盘上仅剩余 1 个绝对空位)时,系统自动、无缝地切入阶段二。

    阶段二:走子阶段(移方)

    • 交互逻辑: 双方轮流选择已在棋盘上的任意一枚己方棋子,将其沿网格线(横向或纵向)平移一格至相邻的唯一空位
    • 限制条件: 严禁斜向移动,严禁跨越或跳跃其他棋子。
    • 阶段终止条件: 触发胜负判定判定(详见3.4节)。

    3.3 阵型判定与吃子规则(核心博弈层)

    游戏的核心奖励机制依赖于几何阵型的达成。

    3.3.1 阵型检测数学模型

    • 成方 (Square): 4枚己方棋子占据任意一个 $1 \times 1$ 最小正方形网格的四个角。
      • 数学判定: 遍历 $\forall x \in [0,3], y \in [0,3]$。若点 $(x,y)$、$(x+1,y)$、$(x,y+1)$、$(x+1,y+1)$ 的所有权均属于同一玩家,则判定该区域“成方”。
      • 战果: 触发吃子机制,允许且强制该玩家吃掉对方 1 枚棋子
    • 成洲 (Row/Column): 己方棋子占满任意一条完整的水平整行或垂直整列(即连续 5 枚子连线)。
      • 数学判定: 遍历 $\forall y \in [0,4]$ 检查 $\sum_{x=0}^{4} P(x,y)$ 是否全属于同一玩家(行检测);同理遍历 $\forall x \in [0,4]$ 检查 $\sum_{y=0}^{4} P(x,y)$(列检测)。
      • 战果: 触发吃子机制,允许且强制该玩家吃掉对方 2 枚棋子
    • 对角线无效原则 (No Diagonal): 任何斜向、对角线方向的连线(无论是 3 子、4 子还是 5 子对角连线)在任何阶段均视为无效阵型。 系统不应对其进行高亮,不触发任何吃子奖励,亦不提供任何防御保护。

    3.3.2 吃子权限与安全护盾原则

    当玩家达成“成方”或“成洲”时,进入吃子状态,选择对方棋子移除,但必须遵循以下防御保护链

    1. 散子优先: 玩家只能优先点击并吃掉对方盘面上的“散子”(即当前不属于任何一个有效“方”或“洲”的独立棋子)。
    2. 护盾生效: 对方所有已经处于“成方”或“成洲”状态中的棋子自动获得免死护盾,处于不可点击(灰色锁定)状态。
    3. 护盾强制解除: 有且仅当对方盘面上所有的存活棋子都已经组成了“方”或“洲”(即全盘无任何散子可吃)时,系统的防御保护链才会强制解除。此时允许玩家自由选择并“破开”对方的方或洲。

    3.3.3 防无限刷子机制:退步不吃子限制 (Anti-Looping)

    为了防止在阶段二(走子阶段)中,玩家利用一枚棋子在两个相邻格之间无脑来回移动(即反复拆方-组方、拆洲-组洲)从而达成恶性无限吃子,引入位置记忆栈:

    • 算法逻辑: 针对每一枚棋子,系统记录其前一步的来源坐标。
    • 限制规则: 假设棋子 $M$ 从坐标 $A$ 移动到坐标 $B$ 达成了“方”或“洲”并执行了吃子。在下一回合中,若玩家将棋子 $M$ 从 $B$ 点原路移回 $A$ 点,即使此时在 $A$ 点的几何几何构型上再次满足了“方”或“洲”的定义,系统也绝对不触发吃子判定
    • 状态重置: 该棋子必须前往除 $A、B$ 之外的第三个合法交叉点,或者在中间回路中有其他棋子改变了 $A$ 点/ $B$ 点周围的整体盘面构型(由全局状态机刷新判定),该棋子的吃子能力才会被重新激活。

    3.4 胜负判定条件 (Winning Conditions)

    满足以下任一条件时,对局状态机立刻终止,并向 GameKit 汇报比赛结果:

    1. 兵力枯竭: 某一方在吃子结算完成后,其盘面剩余的棋子总数严格少于 4 枚。因其已从数学上彻底丧失了组成“方”(需4子)或“洲”(需5子)的基础,直接判负。
    2. 空间憋死(憋方): 轮到某一方走子时,系统遍历该方全盘所有残存棋子,发现其上下左右所有相邻的目标格均被其他棋子占满(无任何合法空位)。该方无路可走,直接判负。
    3. 单局步时超时: 单局对局中,每步限时 30 秒。若某玩家单局累计 3 次超时未做出操作,直接判负。

    4. 基于 GameKit (Game Center) 的网络技术需求

    本项目完全舍弃第三方中转服务器,全盘依托 Apple 原生的 GameKit 框架搭建去中心化联机对战网络。

    4.1 身份验证与登录流

    [App 启动] 
       └── 异步调用 GKLocalPlayer.local.authenticateHandler
             ├── [已登录 Game Center] ──> 获取玩家 ID/昵称 ──> 进入游戏主大厅
             └── [未登录 Game Center] ──> 弹出 iOS 系统原生 Game Center 登录弹窗
                   ├── 用户完成登录 ──> 进入游戏主大厅
                   └── 用户拒绝登录 ──> 弹窗强制提示:“本游戏为纯线上对战,需开启 Game Center”,退出应用
    

    4.2 匹配机制 (Matchmaking)

    大厅提供两种匹配模式,均通过配置 GKMatchRequest 实现:

    • 快速随机匹配:
      • minPlayers = 2, maxPlayers = 2
      • playerGroup = 1 (用于标识标准 5×5 经典摆方玩法,隔离后续衍生版本)
      • 调用 GKMatchmaker.shared().findMatch(for:withCompletionHandler:) 在后台隐式寻找全球在线玩家。
    • 好友定向邀请:
      • 唤起 iOS 原生的 GKMatchmakerViewController。用户可通过内置好友列表、iMessage 链接或生成房间码发送给好友。

    4.3 P2P 数据通信协议与确定性同步

    为应对去中心化架构下的作弊和断线问题,采用确定性状态机(Deterministic State Machine)同步。

    4.3.1 传输模式

    所有核心游戏指令(落子、走子、挑子、认输)必须使用 GKMatchSendDataMode.reliable 模式发送,保证包序正确且绝不丢包。

    4.3.2 协议数据包结构 (Protocol Data Structure)

    每次玩家操作,客户端向对端发送一个经过 JSONEncoder 序列化或二进制打包的 Fixed-size 结构体:

    Swift

    struct BoardActionPacket: Codable {
        let protocolVersion: Int // 协议版本,当前固定为 1
        let actionType: Int      // 动作类型: 0-落子, 1-移子, 2-吃子确认, 3-主动认输, 4-步时超时
        let playerID: String     // 发送方 Game Center 唯一玩家标识符
        let fromIndex: Int       // 起始点一维索引 (0-24)。落子阶段固定为 -1
        let toIndex: Int         // 目标落盘点一维索引 (0-24)。吃子/认输阶段固定为 -1
        let eatIndices: [Int]    // 被选定吃掉的对方棋子索引数组。成方传1个元素,成洲传2个元素,常规走子传空数组
        let timestamp: Int64     // 毫秒级客户端时间戳,用于单步时钟校对
    }
    

    4.3.3 去中心化仲裁

    • 先后手与主节点生成: 匹配建立成功后,两端客户端在毫秒内隐式交换一个全局随机大数(Secure Random)。数字大的一方在第一局作为黑方(先手),并在内存中兼任“逻辑仲裁节点”。
    • 双端校验: 任何一方行动,两端的本地客户端均会独立运行一次“核心游戏规则与算法逻辑”(第3节)。若接收到的 BoardActionPacket 与本地计算的状态不符(例如数据篡改外挂),本地立刻弹窗提示“对局数据异常”,断开 GameKit 连接,对局作废。

    4.4 异常处理与断线断判定 (Connectivity & Exception Handling)

    由于没有服务器维持心跳,断线逻辑完全依赖 GKMatchDelegate 的回调。

    • 断线状态判定: 当触发 match(_:player:didChange:) 且状态变为 .disconnected 时,触发断线保护。
    • 切后台容忍度: 允许玩家因接听电话或误触 Home 键导致 App 进入后台。系统为其保留 15 秒的弹性等待期。
    • 判负裁决:
      • 若断开连接超过 15 秒未重连,或单局内断开次数累计达到 3 次,留存存活的客户端立刻宣布胜利,并弹出原生提示:“对方玩家断开连接,本局获胜”。
      • 胜出方客户端负责向本地写入胜场记录,并调用 match.disconnect() 优雅释放 GameKit 网络通道。

    5. UI/UX 视觉设计与交互需求 (User Interface & Interaction)

    本游戏遵循 Modern Flat 2.0(现代扁平化 2.0) 视觉美学。整体设计原则是:消灭一切高光、纹理和立体厚度,依靠和谐的色块、高级的Whitespace(留白)以及高帧率的动效来构筑空间感与品质感。

    5.1 界面与组件设计

    5.1.1 全局色彩规范 (Color Palette)

    所有界面元素必须严格控制在以下色调范围内:

    • 全局底色 (Background): 极柔和的冷灰白 (#F4F4F6)。
    • 棋盘主线 (Grid Line): 细线,深色哑光灰 (#2C3E50),粗细固定为 1.5pt。
    • 玩家 A(先手/深蓝): 高饱和度、低明度的科技深蓝 (#1A365D),完全扁平圆形。
    • 玩家 B(后手/亮橙): 极具视觉张力的活力橙 (#FF6B35),完全扁平圆形。
    • 状态/成功提示 (Success): 舒缓的薄荷绿 (#2ECC71)。
    • 警告/吃子状态 (Warning): 柔和的警示红 (#E74C3C)。

    5.1.2 核心界面元素 breakdown

    界面名称核心 UI 元素组件交互与视觉规范
    Splash / 登录页1. 几何拼贴式游戏 Logo
    2. 原生 Game Center 登录按钮
    全屏底色。Logo 采用无衬线粗体字结合扁平圆形拼贴。按钮规范采用 iOS 标准圆角。
    Main Lobby / 主大厅1. 个人信息栏 (头像/昵称)
    2. “快速匹配” 胶囊按钮
    3. “与友对战” 胶囊按钮
    极简留白风格。按钮尺寸必须满足 iOS 最低 44x44pt 触控区域要求,采用轻微渐变(Modern Flat 2.0 特色)。
    Game Room / 对局室1. 顶部常驻状态条 (Status Banner)
    2. 中央 5×5 正方形网格棋盘
    3. 双方剩余棋子计分板
    4. 30秒环形倒计时器
    棋盘居中,四周留出安全距离。当前回合玩家的计分板有 2dp 的柔和投影以示高亮。非当前回合棋盘整体覆盖 10% 透明度的灰层。
    Settlement / 结算弹窗1. 扁平化胜利/失败大插图
    2. “返回大厅” 扁平按钮
    弹窗不使用任何传统拟物化高光边框,直接以纯色块叠加在大暗化棋盘之上。

    5.2 交互动态反馈机制 (Micro-interactions)

    为补偿扁平化带来的视觉扁平感,交互必须配备灵敏的动效与硬件反馈。

    5.2.1 触控手势反馈

    • 落子选点提示: 玩家手指在棋盘空白交叉点上滑动或悬停时,该点自动渲染一个半径为棋子 50% 的淡色呼吸灯半透明圆环(颜色与玩家阵营一致),放手或再次点击完成落子,防止大屏 iPhone 上的手指误触。
    • 走子拖拽/点击: 阶段二中,点击己方合法棋子,该棋子原地放大 10%,并以薄荷绿呼吸灯高亮其上下左右可移动的合法空位点。支持拖拽投掷或点击目标点两种操作方式。

    5.2.2 阵型触发与吃子高亮(视觉奇观)

    • 成方/成洲动效: 当横/纵/方连线达成的瞬间,被连线的 4 枚或 5 枚棋子产生一次向外扩散的波纹动效(Ripple Effect),同时连线轨迹由普通灰线变为金色,闪烁 0.3 秒。
    • 吃子引导: 画面其余部分被一律调暗 30%,对方盘面上所有可被吃掉的散子开始进行低频的小幅度红点左右震动(Shake Animation),而受到保护的方/洲子则处于半透明锁定状态。顶部状态栏变为警告红:“请挑拣并吃掉对方 [1/2] 枚棋子”。

    5.2.3 iOS 触觉反馈 (Taptic Engine Natively)

    深度集成 UIFeedbackGenerator,利用 iPhone 线性马达提供以下物理感知:

    • 常规落子/移子成功: 触发 UIImpactFeedbackGenerator(style: .light),给予指尖轻微扎实的卡塔感。
    • 成方 / 成洲奇迹: 触发 UINotificationFeedbackGenerator().notificationOccurred(.success),给予连续两次极具荣誉感的强震动。
    • 进入吃子 / 被吃子状态: 触发 UIImpactFeedbackGenerator(style: .medium)
    • 步时最后 5 秒: 每秒触发一次轻微的 style: .light 脉冲,模拟心跳紧迫感。

    6. 非功能性需求 (Non-Functional Requirements)

    6.1 性能与平台适配

    • 系统版本: 必须原生支持 iOS 15.0 及以上 所有正式发布的主流系统版本。
    • 硬件适配: 适配 iPhone 11、iPhone 12 至最新一代全系 iPhone 设备。针对带有刘海屏、安全岛(灵动岛)以及底部 Home Indicator 的机型,必须严格遵守 SafeAreaLayoutGuide 约束,确保顶部状态栏和底部计分板绝不被物理硬件遮挡。
    • 帧率表现: 游戏底层物理网格渲染与 Flat 2.0 动画过渡必须在所有受支持设备上稳定跑满 60 FPS(支持 ProMotion 的设备需动态适配 120 FPS),确保手势滑动丝滑、无任何掉帧与微卡顿。

    6.2 安全性与公平性 (Fair Play)

    • 内存保护: 本地游戏状态机中的核心关键变量(如 myTokenCount, opponentTokenCount, isMyTurn)必须进行混淆或采用结构化加密存储,严防通过外挂直接修改运行内存。
    • 去中心化时钟同步: 每次网络发包带上严密的递增序列号(Sequence ID)与毫秒级时间戳。客户端如果收到短时间内大量重复或延迟严重的过时指令包,一律视为网络作弊拦截并报错。

    7. 项目一期 MVP 验收标准 (Acceptance Criteria)

    当且仅当以下测试用例在 iOS 真机环境下完全通过,方能推进交付上架流程:

    1. Game Center 链路闭环: 在无中心化服务器参与下,两台处于不同网络(如一端Wi-Fi,一端5G)的 iPhone 能够通过“快速匹配”在 10 秒内成功建立 1v1 对局室。
    2. “方”“洲”逻辑零误判: 在真实对局测试中,系统能精准识别 $1 \times 1$ 四角成方(吃1子)和 5子行列成洲(吃2子);斜对角线连线满5子时,系统无任何响应,完全判定为常规散子。
    3. 吃子保护链无漏洞: 进入吃子阶段时,对方处于成方/成洲中的棋子绝对无法被点击和移除;只有将对方外部散子完全吃光后,保护盾才能自动解开并允许破方。
    4. 反刷子逻辑成功: 在阶段二移动棋子时,同一枚棋子在 A-B 两点连续来回挪动,除第一次构成方/洲触发吃子外,后续来回挪动动作被系统拦截,不产生二次吃子。
    5. 异常断线优雅退场: 正在对局的一方直接杀掉 App 进程或切后台超过 15 秒,另一方客户端能精准在第 15 秒弹出通知,并正确结算自己为胜利方。
  • RxSwift使用

    RxSwift 开发技术文档

    📌 概述

    RxSwift 是面向 Swift 的函数响应式编程(Functional Reactive Programming)框架。它通过将异步事件抽象为可观察序列(Observable),实现跨线程调度、UI 绑定以及复杂逻辑的链式组合。


    🛠 核心组件

    1. 事件源 (Data Sources)

    • Observable: 只读的事件流。最基础的序列。
    • Subject: 既是 Observable 也是 Observer。常用于作为数据桥梁:
      • PublishSubject: 仅接收订阅之后的事件。
      • BehaviorSubject: 必须有初始值,新订阅者会立刻收到最新的那个值。
      • ReplaySubject: 缓存最后 $N$ 个事件,新订阅者能“回放”这些事件。
    • Relay (RxCocoa): 对 Subject 的包装。不会发送 ErrorCompleted 事件,专用于 UI 状态绑定。
      • PublishRelay / BehaviorRelay

    2. 内存管理

    • DisposeBag: 订阅销毁袋。当页面(ViewController)销毁时,袋子内的所有订阅会自动断开,防止内存泄漏

    🚀 核心使用场景与代码模板

    场景 A:UI 事件监听与绑定 (RxCocoa)

    将按钮点击、输入框输入等 UI 事件直接绑定到业务逻辑或另一个 UI 控件。

    import UIKit
    import RxSwift
    import RxCocoa
    
    class LoginViewController: UIViewController {
        @IBOutlet weak var usernameTextField: UITextField!
        @IBOutlet weak var loginButton: UIButton!
        
        private let disposeBag = DisposeBag()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 示例 1: 输入字数限制与按钮状态联动
            usernameTextField.rx.text.orEmpty
                .map { $0.count >= 6 }                 // 转换成 Bool
                .bind(to: loginButton.rx.isEnabled)    // 直接绑定到按钮的启用状态
                .disposed(by: disposeBag)              // 自动加入销毁袋
                
            // 示例 2: 按钮点击事件响应
            loginButton.rx.tap
                .subscribe(onNext: { [weak self] in
                    self?.executeLoginLogic()
                })
                .disposed(by: disposeBag)
        }
        
        private func executeLoginLogic() {
            print("开始登录...")
        }
    }
    

    场景 B:网络请求转换 (数据流映射)

    利用操作符将原始网络请求结果转换成 UI 需要的模型。

    import RxSwift
    
    struct User: Codable {
        let name: String
    }
    
    class UserService {
        // 模拟网络请求:将回调封装为 Observable
        func fetchUser() -> Observable<Data> {
            return Observable.create { observer in
                let mockJson = "{\"name\": \"张三\"}".data(using: .utf8)!
                observer.onNext(mockJson)
                observer.onCompleted()
                return Disposables.create()
            }
        }
    }
    
    // 业务调用层
    let service = UserService()
    let bag = DisposeBag()
    
    service.fetchUser()
        .map { data -> User in
            // 操作符:将 Data 解析为 Model
            return try JSONDecoder().decode(User.self, from: data)
        }
        .subscribe(onNext: { user in
            print("成功解析出用户: \(user.name)")
        }, onError: { error in
            print("错误处理: \(error)")
        })
        .disposed(by: bag)
    

    场景 C:多线程调度 (Schedulers)

    在后台处理耗时任务,在主线程更新 UI。

    import RxSwift
    
    someBackgroundHeavyTask()
        .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) // 1. 指定在后台线程执行任务
        .observeOn(MainScheduler.instance)                               // 2. 切换回主线程
        .subscribe(onNext: { result in
            // 3. 安全地更新 UI
            self.statusLabel.text = "处理完成: \(result)"
        })
        .disposed(by: disposeBag)
    

    ⚠️ 避坑指南与最佳实践

    1. 循环引用问题:在 subscribe(onNext:) 闭包中使用 self 时,务必使用 [weak self],否则会导致控制器无法释放。
    2. UI 必须在主线程:所有涉及 UI 绑定的链条,在最终 bindsubscribe 之前,确保加上 .observeOn(MainScheduler.instance)(或者直接使用 RxCocoa 的 Driver 机制,它自带主线程确保)。
    3. 不要漏掉 .disposed(by: disposeBag):如果忘记添加销毁袋,序列将永久常驻内存。

    如果想继续完善这份文档,请告诉我您更希望深入哪部分:是 ViewModel (MVVM) 架构的实战组合,还是更高级的操作符(如 combineLatest, flatMap)解析

  • 世界,您好!

    欢迎使用 WordPress。这是您的第一篇文章。编辑或删除它,然后开始写作吧!