为你整理了一份结构化的技术需求说明书(PRD/Tech Spec)。这份文档高度浓缩了我们刚才讨论的 oTree 5.11 实现细节。 你可以直接将以下被分割线包含的内容复制并发送给任何 LLM(包括我),用来作为零门槛生成完整底层代码的 Context 提示词。 --- # oTree 实验需求与技术实现文档 (面向 LLM 的开发指南) ## 1. 项目概述 本项目是一个基于 oTree (v5.11+) 开发的经济学实验。实验的核心需求是在**同一个 Session 内混合运行单人 (Single) 和双人团队 (Team) 的多重 Treatment**。 为了实现动态分组和代码解耦,项目将分为两个阶段(即两个 App): * **App 1 (Assignment App)**:负责基于玩家到达顺序(Arrival Time)按特定比例分配 Treatment 标签,并注入分组规则。 * **App 2 (Task App)**:负责根据 App 1 注入的规则进行动态组队(GBAT),并执行实验核心逻辑。 ## 2. 实验设计与分配规则 * **Treatments (共 5 个)**: * 单人组 (Single):`Single_A`, `Single_B` (2 个) * 双人组 (Team):`Team_A`, `Team_B`, `Team_C` (3 个) * **分配顺序门槛 (Threshold Logic)**: * 优先填满单人组:Session 的前 40%(即 2/5)到达的玩家,全部分配到单人组。 * 然后填满双人组:超过 40% 门槛后到达的玩家,全部分配到双人组。 * **分配内部逻辑**: * 单人组阶段:在 `Single_A` 和 `Single_B` 之间交替轮流分配(如 A, B, A, B...)。 * 双人组阶段:为了保证玩家在 App 2 能成功匹配,必须**成对分配**同样的 Treatment。即连续两人分配 `Team_A`,接着两人分配 `Team_B`,以此类推(如 A, A, B, B, C, C...)。 ## 3. 技术实现细节规范 ### 3.1 App 1: 分配逻辑 (Assignment App) * **核心机制**:使用 `group_by_arrival_time_method` (GBAT) 进行动态拦截与分配。 * **全局计数器**:为了记录到达顺序,需使用 `session.vars.setdefault('arrived_count', 0)` 作为全局计数器(不要使用 `subsession.player_count` 以防多轮次问题)。 * **状态注入 (State Injection)**:计算出玩家对应的 Treatment 后,必须将以下两个关键变量存入 `participant.vars`,供后续 App 使用: 1. `player.participant.vars['treatment']`:具体的 Treatment 名称。 2. `player.participant.vars['expected_group_size']`:单人组设为 `1`,双人组设为 `2`。 * **Debug 支持**:需保留读取 `subsession.session.config.get('treatment')` 进行强制覆盖的后门逻辑。 ### 3.2 App 2: 任务与组队逻辑 (Task App) * **组队机制**: * `Constants.PLAYERS_PER_GROUP = None`(必须设为 None 以支持动态不规则组队)。 * 使用 `group_by_arrival_time_method`。 * **GBAT 算法要求**: * **完全解耦**:App 2 的 GBAT 内部**禁止**硬编码任何具体的 Treatment 名称(如 `Single_A`)。 * **异常处理**:优先拦截并放行 `is_dropout` 或 `waiting_too_long` 的玩家。 * **动态匹配**:按照 `participant.vars['treatment']` 将等待列表(`waiting_players`)中的玩家归类到字典中。读取 `participant.vars['expected_group_size']`。当某类 Treatment 的等待人数 $\ge$ 该变量时,将其打包返回(发车)。 * **页面控制 (Pages)**: * 实验包含填写决策的页面(如 `DecisionPage`)。 * 包含一个等待队友和计算平均值的 WaitPage(如 `TeamWaitPage`)。 * **条件显示**:`TeamWaitPage` 必须且仅能通过 `@staticmethod def is_displayed(player):` 来控制。判断条件为 `player.participant.vars.get('expected_group_size') == 2`。单人组将无感跳过此等待页。 ## 4. 边界条件与防错要求 (Edge Cases) 1. **总人数非完美倍数**:在 App 1 中使用 `int(session.num_participants * 0.4)` 计算门槛。如果后续双人组阶段最后出现落单玩家(单数),App 2 的超时逻辑 (`waiting_too_long`) 会将其安全释放。 2. **WaitPage 数据计算**:在 `TeamWaitPage` 的 `after_all_players_arrive(group)` 中,处理双方的决策平均值。单人组由于跳过此页面,直接在最终的 `Results` 页面调用自身的决策值。 ## 5. 提示词执行指令 (Prompt for LLM) 作为 AI 开发者,请根据以上需求文档,为我生成 App 1 和 App 2 的 `__init__.py` 完整核心代码。重点确保 App 1 的按比例成对分配算法(使用索引取模)的准确性,以及 App 2 分组逻辑的纯粹性与性能。代码需严格遵守 oTree 5.x 的无类方法(no-self)新语法。 ---