├── demo ├── .gitkeep ├── snake_game_v2_complete.py ├── js │ ├── vector3.js │ ├── camera.js │ ├── matrix4.js │ ├── car.js │ ├── track.js │ ├── utils.js │ ├── game.js │ ├── renderer.js │ ├── physics.js │ ├── sound.js │ └── particles.js ├── index.html ├── style.css ├── styles.css ├── catch_game.html ├── racing_game.html ├── script.js ├── gobang_snake.html └── game.js ├── requirements.txt ├── articles ├── v2文章.md └── v1文章.md ├── README.md ├── README_en.md ├── .gitignore └── v1_basic_agent.py /demo/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame==2.5.2 2 | numpy==1.24.3 -------------------------------------------------------------------------------- /demo/snake_game_v2_complete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 超级贪吃蛇游戏 v2.0 - 完整版 5 | ===================================== 6 | 7 | 这是一个功能丰富的贪吃蛇游戏,包含以下特性: 8 | 9 | 🎮 游戏模式: 10 | • 经典模式 - 传统贪吃蛇玩法 11 | • AI对战模式 - 与智能AI对战 12 | • 生存模式 - 带障碍物的挑战模式 13 | • 时间挑战模式 - 限时达成目标 14 | • 无尽模式 - 持续增加的难度 15 | • 多人对战模式 - 本地双人游戏 16 | • 关卡编辑器 - 创建自定义关卡 17 | 18 | 🚀 高级功能: 19 | • A*寻路AI算法 20 | • 游戏进度保存/读取 (F5/F9) 21 | • 成就系统 22 | • 粒子特效 23 | • 音效系统 24 | • 传送门机制 25 | • 地雷陷阱 26 | • 多种道具系统 27 | 28 | 🎯 道具类型: 29 | • 速度提升 - 移动速度加快 30 | • 双倍积分 - 得分翻倍 31 | • 护盾 - 免疫一次伤害 32 | • 幽灵模式 - 穿墙能力 33 | • 传送 - 瞬移到安全位置 34 | • 炸弹 - 清除周围障碍物 35 | • 时间冻结 - 暂停时间计时 36 | 37 | 🎨 视觉效果: 38 | • 动态粒子系统 39 | • 传送门动画 40 | • 护盾光环效果 41 | • 渐变蛇身颜色 42 | • 脉冲道具动画 43 | 44 | 📊 游戏统计: 45 | • 详细的游戏数据统计 46 | • 成就追踪 47 | • 关卡解锁系统 48 | • 高分记录 49 | 50 | 控制方式: 51 | --------- 52 | 玩家1: 53 | 方向键 - 移动 54 | B - 使用炸弹 55 | T - 使用传送 56 | 空格 - 暂停/继续 57 | F5 - 快速保存 58 | F9 - 快速加载 59 | ESC - 退出游戏 60 | 61 | 玩家2 (多人模式): 62 | WASD - 移动 63 | 64 | 关卡编辑器: 65 | --------- 66 | 1-5 - 选择工具 67 | 左键 - 放置元素 68 | 右键 - 移除元素 69 | S - 保存关卡 70 | C - 清空关卡 71 | G - 切换网格吸附 72 | H - 显示/隐藏帮助 73 | ESC - 退出编辑器 74 | 75 | 作者: AI Assistant 76 | 版本: 2.0 77 | """ 78 | 79 | # 合并所有代码到一个文件 80 | exec(open("snake_game_v2.py").read()) 81 | exec(open("snake_game_v2_main.py").read()) 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /articles/v2文章.md: -------------------------------------------------------------------------------- 1 | # mini Kode v2:让模型自我约束的 Todo 工具 2 | 3 | v1 版本的 mini Kode 已经证明:只要把大模型嵌入一个简洁的工具循环,它就能像“代码工人”一样在本地仓库里读写文件、执行命令。但我们也遇到一个老问题——**模型是否真的在有计划地推进任务?** 4 | 5 | 顺便推荐一下我们维护的 Kode 项目:这是面向生产的开源版 Claude Code,收录了 Bash 扩展、WebSearch / WebFetch、Docker 适配等高级能力,适合直接落地企业级场景。若你在寻找更多资料,也可以参考我们先前整理的一系列开源实现与逆向分析,它们共同构成了如今 mini Kode 的“孵化器”。 6 | 7 | 在 v2 中,我们引入了一个看似朴素、却改变工作流体验的核心能力:**Todo 工具链**。它把“规划—执行—复盘”的节奏落在了代码级别,迫使模型对复杂任务进行显式拆解,从而让用户和模型都能清楚地看到每一步发生了什么。 8 | 9 | ## 1. 具体做了什么? 10 | 11 | - **新增 TodoWrite 工具与 TodoManager**:在工具列表中加入 `TodoWrite`,由 `TodoManager` 维护一个至多 20 条的任务列表,约束只允许一个条目处于 `in_progress`,并在每次更新时返回彩色状态视图与统计数据。 12 | - **消息循环挂钩待办状态**:在每轮模型输出后,如果出现对 Todo 的调用,就把更新结果作为工具反馈写回对话历史,让模型看到自己刚刚操作的“计划板”,形成自监督闭环。 13 | - **系统提醒机制**:在会话伊始植入 system reminder,如果连续 10 轮交互未触发 Todo,再次注入温和提醒,让模型持续感知“规划要求”;每次真正更新 Todo 后则重置计数器。 14 | - **严格输入校验**:对 Todo 输入的 `id`、`status`、`activeForm` 等字段进行检查,防止模型写入空任务、重复条目或异常状态,引导它遵循清晰的规范。 15 | 16 | ## 2. 背后的思想 17 | 18 | > **模型依旧是核心,但要用规则把它“拴”在结构化工作流里。** 19 | 20 | 1. **结构化约束代替自由生成**:单纯的文本指令很容易让模型忘记计划,Todo 工具则把“计划”实体化为数据结构,模型只有维护好它才能继续执行。 21 | 2. **显式反馈增强自我监督**:每次 Todo 更新都会立即显示当前状态,模型能在下一轮读到这段文本,相当于用环境反馈提醒它“你现在在做哪一步”。 22 | 3. **系统提示形成软约束**:提醒块不是条件语句,而是上下文资料;它们用最小侵入的方式告诉模型“别忘记 Todo”,既保留灵活性,又能降低跑偏概率。 23 | 24 | ## 3. 能应用到哪些场景? 25 | 26 | - **多步骤代码任务**:例如“写接口 → 补测试 → 更新文档”,模型必须先把步骤写进 Todo,然后逐项勾选,用户可以随时检查进度。 27 | - **长周期对话**:在需要几十轮推演的需求中,system reminder 会周期性提醒模型遵守规划流程,避免它在自由文本里迷失。 28 | - **团队协同与审计**:把 Todo 输出存档,就能追踪模型在每次会话里的计划与执行痕迹,为回顾和复盘提供依据。 29 | 30 | v1 的意义在于验证“模型 as agent”这一底层设计;v2 则进一步证明:**只要给模型配备合适的结构化工具,它不仅能执行,更能规划、记忆和自我纠错**。后续我们还会继续叠加 Task 子代理与更丰富的 system reminder 组合,并把 Kode 里的成熟特性不断反馈到 mini Kode,让它逐步靠近真实产品级 Agent 的形态。 31 | -------------------------------------------------------------------------------- /demo/js/vector3.js: -------------------------------------------------------------------------------- 1 | class Vector3 { 2 | constructor(x = 0, y = 0, z = 0) { 3 | this.x = x; 4 | this.y = y; 5 | this.z = z; 6 | } 7 | 8 | clone() { 9 | return new Vector3(this.x, this.y, this.z); 10 | } 11 | 12 | add(v) { 13 | return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z); 14 | } 15 | 16 | subtract(v) { 17 | return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); 18 | } 19 | 20 | multiply(scalar) { 21 | return new Vector3(this.x * scalar, this.y * scalar, this.z * scalar); 22 | } 23 | 24 | divide(scalar) { 25 | return new Vector3(this.x / scalar, this.y / scalar, this.z / scalar); 26 | } 27 | 28 | dot(v) { 29 | return this.x * v.x + this.y * v.y + this.z * v.z; 30 | } 31 | 32 | cross(v) { 33 | return new Vector3( 34 | this.y * v.z - this.z * v.y, 35 | this.z * v.x - this.x * v.z, 36 | this.x * v.y - this.y * v.x 37 | ); 38 | } 39 | 40 | length() { 41 | return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); 42 | } 43 | 44 | normalize() { 45 | const len = this.length(); 46 | if (len === 0) return new Vector3(0, 0, 0); 47 | return this.divide(len); 48 | } 49 | 50 | distance(v) { 51 | return this.subtract(v).length(); 52 | } 53 | 54 | rotateY(angle) { 55 | const cos = Math.cos(angle); 56 | const sin = Math.sin(angle); 57 | return new Vector3( 58 | this.x * cos - this.z * sin, 59 | this.y, 60 | this.x * sin + this.z * cos 61 | ); 62 | } 63 | 64 | toString() { 65 | return `(${this.x.toFixed(2)}, ${this.y.toFixed(2)}, ${this.z.toFixed(2)})`; 66 | } 67 | } -------------------------------------------------------------------------------- /demo/js/camera.js: -------------------------------------------------------------------------------- 1 | class Camera { 2 | constructor(fov = 60, aspect = 1, near = 0.1, far = 1000) { 3 | this.position = new Vector3(0, 5, -10); 4 | this.target = new Vector3(0, 0, 0); 5 | this.up = new Vector3(0, 1, 0); 6 | 7 | this.fov = fov * Math.PI / 180; 8 | this.aspect = aspect; 9 | this.near = near; 10 | this.far = far; 11 | 12 | this.viewMatrix = new Matrix4(); 13 | this.projectionMatrix = new Matrix4(); 14 | this.viewProjectionMatrix = new Matrix4(); 15 | 16 | this.followDistance = 15; 17 | this.followHeight = 8; 18 | this.smoothness = 0.1; 19 | } 20 | 21 | setAspectRatio(aspect) { 22 | this.aspect = aspect; 23 | this.updateProjectionMatrix(); 24 | } 25 | 26 | updateProjectionMatrix() { 27 | this.projectionMatrix.identity().perspective(this.fov, this.aspect, this.near, this.far); 28 | } 29 | 30 | updateViewMatrix() { 31 | this.viewMatrix.identity().lookAt(this.position, this.target, this.up); 32 | } 33 | 34 | updateMatrices() { 35 | this.updateViewMatrix(); 36 | this.updateProjectionMatrix(); 37 | this.viewProjectionMatrix.copy(this.projectionMatrix).multiply(this.viewMatrix); 38 | } 39 | 40 | followCar(car) { 41 | const idealPosition = car.position 42 | .add(new Vector3(0, this.followHeight, 0)) 43 | .subtract(car.forward.multiply(this.followDistance)); 44 | 45 | this.position = this.position.add(idealPosition.subtract(this.position).multiply(this.smoothness)); 46 | this.target = car.position.add(new Vector3(0, 2, 0)); 47 | 48 | this.updateMatrices(); 49 | } 50 | 51 | worldToScreen(worldPos) { 52 | const clipPos = this.viewProjectionMatrix.transformVector(worldPos); 53 | 54 | return { 55 | x: (clipPos.x + 1) * 0.5, 56 | y: (1 - clipPos.y) * 0.5, 57 | z: clipPos.z 58 | }; 59 | } 60 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 3D赛车游戏 7 | 67 | 68 | 69 |
70 |

3D赛车游戏

71 | 72 |
73 | 74 | 79 | 80 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 手搓迷你Kode【青春版】 2 | Languages: [中文](README.md) | [English](README_en.md) 3 | 正式版:[Kode - 开源Agent CLI 编码 & 运维工具](https://github.com/shareAI-lab/Kode) 4 | image
5 | Fellow us on X: https://x.com/baicai003 6 | 7 | image 8 | image 9 | 10 | image 11 | 12 | 本仓库展示了一个逐步迭代的「迷你 Kode」实现。我们用最少的代码复刻官方工作流的关键体验,分为两个主要版本: 13 | 14 | - **v1 基础版**:聚焦「模型即代理(Model as Agent)」的核心循环,让大模型在一个最小可用的 CLI 中读取、编辑、写入文件并执行命令。 15 | - **v2 待办版**:在 v1 的基础上,引入结构化计划工具(Todo)、系统提醒等机制,使模型具备显式规划和自我监督能力。 16 | 17 | 以下内容将详细介绍两个版本的运转原理,以及它们之间的思想差异。 18 | 19 | --- 20 | 21 | ## v1:Model as Agent 的最小实现 22 | 23 | v1 版本的目标是验证“代码只负责提供工具,大模型才是唯一的行为主体”这一理念。整个 CLI 只有约 400 行 Python 代码,却包含了 Agent 最关键的要素: 24 | 25 | ### 1. 系统角色设定 26 | - 通过 `SYSTEM` 字符串明确约束模型:以仓库为工作区、优先用工具行动、结束时要总结。 27 | - 这些规则让模型在长对话里始终记得“该做事”而不是“闲聊”。 28 | 29 | ### 2. 统一的工具调度 30 | - 预置了四个核心工具:`bash`、`read_file`、`write_file`、`edit_text`。 31 | - 调度逻辑会根据模型的 `tool_use` 块调用对应的执行函数,输出结果再以 `tool_result` 写回对话。 32 | - 所有工具都带有安全检查(路径约束、危险命令禁用、输出裁剪),确保运行可控。 33 | 34 | ### 3. 控制台体验 35 | - `Spinner` 线程在模型推理时显示等待动画。 36 | - `pretty_tool_line` 和 `pretty_sub_line` 负责把每次工具调用的标题与输出排版成易读的格式。 37 | - 与模型的对话历史被完整保留在 `messages` 中,确保上下文一致性。 38 | 39 | ### 4. 主循环行为 40 | - CLI 启动后提示工作目录,用户输入即时追加到 `history`。 41 | - 每轮调用 `client.messages.create`,若模型请求使用工具,则递归处理直到返回最终文本。 42 | - 异常处理通过 `log_error_debug` 记录,避免 CLI 直接崩溃。 43 | 44 | > **核心理念:** 只要提供一个稳定的“工具壳”,模型就能主动完成绝大多数编码任务。v1 证明了 Kode 成功的秘诀不在各种节点雕花流转,而在于让模型保持连续上下文 + 拥有持续调用工具的能力。 45 | 46 | --- 47 | 48 | ## v2:结构化规划与系统提醒 49 | 50 | v2 在 v1 的基础上,重点解决“模型如何保持有序规划”这个问题。我们新增了 Todo 工具链与系统提醒机制,让模型始终处于结构化、可追踪的工作流中。 51 | 52 | ### 1. Todo 工具链 53 | - **`TodoManager`**:维护最多 20 条待办,强制唯一的 `in_progress` 条目,并对输入做严格校验(ID、状态、描述、表单名称)。 54 | - **`TodoWrite` 工具**:模型可以调用它来创建、更新、完成待办,CLI 会即时渲染彩色状态并输出统计结果。 55 | - **状态可视化**: 56 | - `pending` → 柔和灰色 57 | - `in_progress` → 高亮蓝色 58 | - `completed` → 绿色并带删除线 59 | 60 | 通过 Todo 面板,模型和用户都能清楚看到当前计划、正在进行的步骤以及已完成情况。 61 | 62 | ### 2. System Reminder(系统提醒) 63 | - **初始提醒**:会话开始前,将「请使用 Todo 工具管理多步骤任务」作为特殊上下文块注入。 64 | - **周期提醒**:如果连续 10 轮对话没有 Todo 更新,会再注入一次提醒,温和提示模型恢复规划。 65 | - **自动重置**:每当 Todo 有更新,计数器归零,避免重复“催促”。 66 | 67 | 这一机制确保模型不会在长对话里忘记使用 Todo,也让用户免于手动监督。 68 | 69 | ### 3. 交互流程改动 70 | - 输入阶段:将用户消息与待发送的提醒块合并为同一组 content,维护对话一致性。 71 | - 输出阶段:Todo 工具的结果即时打印,并写入历史,让模型在下一轮能够“看到”自己刚刚更新的计划。 72 | - 文案统一:所有提示与汇报改为英文,保持更适合海外产品的风格(代码中不再包含中文)。 73 | 74 | ### 4. 设计收益 75 | - **结构化约束**:模型必须先规划再执行,避免“想到哪写到哪”。 76 | - **自我监督**:Todo 视图是模型的外部记忆,持续提醒它当前所处的步骤。 77 | - **可追溯**:整个会话的待办记录可用于回顾与审计。 78 | 79 | > **扩展链接**:如果想体验生产级的 Claude Code 工作流,推荐试试我们维护的开源项目 [Kode](https://github.com/shareAI-lab/Kode)。它在这个迷你仓库的基础上加入了 Bash 扩展、WebSearch/WebFetch、Docker 支持、IDE 插件等高级功能。 80 | 81 | --- 82 | 83 | ## 总结 84 | 85 | - **v1 思想**:构建最小工具循环,证明“模型即代理”的可行性。 86 | - **v2 思想**:在最小循环上加固结构化规划与系统提醒,让模型具备可视化的计划能力和自我约束力。 87 | 88 | 接下来,我们还计划在 mini Kode 中逐步引入 Task 子代理、更多类型的提醒矩阵等能力,并把 Kode 项目中沉淀的实践回流到这个教学仓库,帮助更多开发者快速理解并构建自己的智能代理系统。 89 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Arial', sans-serif; 9 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 10 | min-height: 100vh; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | color: white; 15 | } 16 | 17 | .container { 18 | text-align: center; 19 | padding: 20px; 20 | background: rgba(255, 255, 255, 0.1); 21 | border-radius: 20px; 22 | backdrop-filter: blur(10px); 23 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 24 | border: 1px solid rgba(255, 255, 255, 0.2); 25 | } 26 | 27 | h1 { 28 | margin-bottom: 20px; 29 | font-size: 2.5em; 30 | text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); 31 | background: linear-gradient(45deg, #4facfe, #00f2fe); 32 | -webkit-background-clip: text; 33 | -webkit-text-fill-color: transparent; 34 | background-clip: text; 35 | } 36 | 37 | .controls { 38 | margin-bottom: 20px; 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | gap: 15px; 43 | flex-wrap: wrap; 44 | } 45 | 46 | .controls label { 47 | font-weight: bold; 48 | font-size: 14px; 49 | } 50 | 51 | .controls input[type="range"] { 52 | width: 100px; 53 | height: 5px; 54 | border-radius: 5px; 55 | background: rgba(255, 255, 255, 0.3); 56 | outline: none; 57 | -webkit-appearance: none; 58 | } 59 | 60 | .controls input[type="range"]::-webkit-slider-thumb { 61 | -webkit-appearance: none; 62 | appearance: none; 63 | width: 20px; 64 | height: 20px; 65 | border-radius: 50%; 66 | background: linear-gradient(45deg, #4facfe, #00f2fe); 67 | cursor: pointer; 68 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); 69 | } 70 | 71 | .controls input[type="range"]::-moz-range-thumb { 72 | width: 20px; 73 | height: 20px; 74 | border-radius: 50%; 75 | background: linear-gradient(45deg, #4facfe, #00f2fe); 76 | cursor: pointer; 77 | border: none; 78 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); 79 | } 80 | 81 | .controls span { 82 | font-weight: bold; 83 | color: #00f2fe; 84 | min-width: 30px; 85 | } 86 | 87 | #resetBtn { 88 | padding: 8px 16px; 89 | background: linear-gradient(45deg, #ff6b6b, #ee5a24); 90 | color: white; 91 | border: none; 92 | border-radius: 20px; 93 | cursor: pointer; 94 | font-weight: bold; 95 | transition: all 0.3s ease; 96 | box-shadow: 0 4px 15px rgba(238, 90, 36, 0.3); 97 | } 98 | 99 | #resetBtn:hover { 100 | transform: translateY(-2px); 101 | box-shadow: 0 6px 20px rgba(238, 90, 36, 0.4); 102 | } 103 | 104 | #waterCanvas { 105 | border: 3px solid rgba(255, 255, 255, 0.3); 106 | border-radius: 15px; 107 | background: linear-gradient(135deg, #1e3c72, #2a5298); 108 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); 109 | cursor: crosshair; 110 | transition: all 0.3s ease; 111 | } 112 | 113 | #waterCanvas:hover { 114 | box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4); 115 | transform: translateY(-2px); 116 | } 117 | 118 | .info { 119 | margin-top: 15px; 120 | font-size: 14px; 121 | color: rgba(255, 255, 255, 0.8); 122 | font-style: italic; 123 | } 124 | 125 | @media (max-width: 768px) { 126 | .container { 127 | padding: 15px; 128 | margin: 10px; 129 | } 130 | 131 | h1 { 132 | font-size: 2em; 133 | } 134 | 135 | #waterCanvas { 136 | width: 100%; 137 | max-width: 600px; 138 | height: auto; 139 | } 140 | 141 | .controls { 142 | gap: 10px; 143 | } 144 | 145 | .controls input[type="range"] { 146 | width: 80px; 147 | } 148 | } 149 | 150 | @media (max-width: 480px) { 151 | h1 { 152 | font-size: 1.5em; 153 | } 154 | 155 | .controls { 156 | flex-direction: column; 157 | gap: 8px; 158 | } 159 | 160 | .controls label { 161 | font-size: 12px; 162 | } 163 | } -------------------------------------------------------------------------------- /demo/js/matrix4.js: -------------------------------------------------------------------------------- 1 | class Matrix4 { 2 | constructor() { 3 | this.elements = [ 4 | 1, 0, 0, 0, 5 | 0, 1, 0, 0, 6 | 0, 0, 1, 0, 7 | 0, 0, 0, 1 8 | ]; 9 | } 10 | 11 | identity() { 12 | this.elements = [ 13 | 1, 0, 0, 0, 14 | 0, 1, 0, 0, 15 | 0, 0, 1, 0, 16 | 0, 0, 0, 1 17 | ]; 18 | return this; 19 | } 20 | 21 | copy(m) { 22 | this.elements = m.elements.slice(); 23 | return this; 24 | } 25 | 26 | multiply(m) { 27 | const a = this.elements; 28 | const b = m.elements; 29 | const result = new Array(16); 30 | 31 | for (let i = 0; i < 4; i++) { 32 | for (let j = 0; j < 4; j++) { 33 | result[i * 4 + j] = 0; 34 | for (let k = 0; k < 4; k++) { 35 | result[i * 4 + j] += a[i * 4 + k] * b[k * 4 + j]; 36 | } 37 | } 38 | } 39 | 40 | this.elements = result; 41 | return this; 42 | } 43 | 44 | translate(x, y, z) { 45 | const translation = new Matrix4(); 46 | translation.elements[12] = x; 47 | translation.elements[13] = y; 48 | translation.elements[14] = z; 49 | return this.multiply(translation); 50 | } 51 | 52 | rotateX(angle) { 53 | const cos = Math.cos(angle); 54 | const sin = Math.sin(angle); 55 | const rotation = new Matrix4(); 56 | rotation.elements[5] = cos; 57 | rotation.elements[6] = -sin; 58 | rotation.elements[9] = sin; 59 | rotation.elements[10] = cos; 60 | return this.multiply(rotation); 61 | } 62 | 63 | rotateY(angle) { 64 | const cos = Math.cos(angle); 65 | const sin = Math.sin(angle); 66 | const rotation = new Matrix4(); 67 | rotation.elements[0] = cos; 68 | rotation.elements[2] = sin; 69 | rotation.elements[8] = -sin; 70 | rotation.elements[10] = cos; 71 | return this.multiply(rotation); 72 | } 73 | 74 | rotateZ(angle) { 75 | const cos = Math.cos(angle); 76 | const sin = Math.sin(angle); 77 | const rotation = new Matrix4(); 78 | rotation.elements[0] = cos; 79 | rotation.elements[1] = -sin; 80 | rotation.elements[4] = sin; 81 | rotation.elements[5] = cos; 82 | return this.multiply(rotation); 83 | } 84 | 85 | scale(x, y, z) { 86 | const scaling = new Matrix4(); 87 | scaling.elements[0] = x; 88 | scaling.elements[5] = y; 89 | scaling.elements[10] = z; 90 | return this.multiply(scaling); 91 | } 92 | 93 | perspective(fov, aspect, near, far) { 94 | const f = 1.0 / Math.tan(fov / 2); 95 | const rangeInv = 1 / (near - far); 96 | 97 | this.elements = [ 98 | f / aspect, 0, 0, 0, 99 | 0, f, 0, 0, 100 | 0, 0, (near + far) * rangeInv, -1, 101 | 0, 0, near * far * rangeInv * 2, 0 102 | ]; 103 | return this; 104 | } 105 | 106 | lookAt(eye, center, up) { 107 | const f = center.subtract(eye).normalize(); 108 | const s = f.cross(up).normalize(); 109 | const u = s.cross(f); 110 | 111 | this.elements = [ 112 | s.x, u.x, -f.x, 0, 113 | s.y, u.y, -f.y, 0, 114 | s.z, u.z, -f.z, 0, 115 | -s.dot(eye), -u.dot(eye), f.dot(eye), 1 116 | ]; 117 | return this; 118 | } 119 | 120 | transformVector(v) { 121 | const x = v.x * this.elements[0] + v.y * this.elements[4] + v.z * this.elements[8] + this.elements[12]; 122 | const y = v.x * this.elements[1] + v.y * this.elements[5] + v.z * this.elements[9] + this.elements[13]; 123 | const z = v.x * this.elements[2] + v.y * this.elements[6] + v.z * this.elements[10] + this.elements[14]; 124 | const w = v.x * this.elements[3] + v.y * this.elements[7] + v.z * this.elements[11] + this.elements[15]; 125 | 126 | return new Vector3(x / w, y / w, z / w); 127 | } 128 | } -------------------------------------------------------------------------------- /demo/js/car.js: -------------------------------------------------------------------------------- 1 | class Car { 2 | constructor() { 3 | this.position = new Vector3(0, 0.5, 0); 4 | this.velocity = new Vector3(0, 0, 0); 5 | this.acceleration = new Vector3(0, 0, 0); 6 | 7 | this.forward = new Vector3(0, 0, 1); 8 | this.right = new Vector3(1, 0, 0); 9 | this.up = new Vector3(0, 1, 0); 10 | 11 | this.speed = 0; 12 | this.maxSpeed = 200; 13 | this.accelerationForce = 100; 14 | this.brakeForce = 150; 15 | this.friction = 0.95; 16 | this.turnSpeed = 0.03; 17 | 18 | this.steering = 0; 19 | this.throttle = 0; 20 | this.brake = 0; 21 | 22 | this.width = 2; 23 | this.height = 1; 24 | this.length = 4; 25 | 26 | this.wheelBase = 2.5; 27 | this.trackWidth = 1.8; 28 | 29 | this.onGround = true; 30 | this.mass = 1000; 31 | 32 | this.wheelRotation = 0; 33 | this.bodyRoll = 0; 34 | this.bodyPitch = 0; 35 | 36 | this.color = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0'); 37 | } 38 | 39 | update(deltaTime, input) { 40 | this.handleInput(input); 41 | this.updatePhysics(deltaTime); 42 | this.updateVisuals(deltaTime); 43 | } 44 | 45 | handleInput(input) { 46 | this.throttle = 0; 47 | this.brake = 0; 48 | this.steering = 0; 49 | 50 | if (input.forward) this.throttle = 1; 51 | if (input.backward) this.brake = 1; 52 | if (input.left) this.steering = -1; 53 | if (input.right) this.steering = 1; 54 | if (input.handbrake) this.brake = 1.5; 55 | 56 | if (input.reset) { 57 | this.reset(); 58 | } 59 | } 60 | 61 | updatePhysics(deltaTime) { 62 | const dt = deltaTime; 63 | 64 | if (this.speed > 0.1) { 65 | const turnAmount = this.steering * this.turnSpeed * (this.speed / this.maxSpeed); 66 | this.forward = this.forward.rotateY(turnAmount); 67 | this.right = this.forward.cross(this.up); 68 | } 69 | 70 | const engineForce = this.throttle * this.accelerationForce; 71 | const brakeForce = this.brake * this.brakeForce; 72 | 73 | const dragForce = this.speed * this.speed * 0.001; 74 | const rollingResistance = this.speed * 0.5; 75 | 76 | const totalForce = engineForce - brakeForce - dragForce - rollingResistance; 77 | 78 | this.speed += totalForce * dt; 79 | this.speed = Math.max(0, Math.min(this.speed, this.maxSpeed)); 80 | 81 | if (this.speed < 0.1 && this.brake > 0) { 82 | this.speed = Math.max(this.speed - this.brakeForce * dt * 0.5, -this.maxSpeed * 0.3); 83 | } 84 | 85 | this.velocity = this.forward.multiply(this.speed); 86 | 87 | this.position = this.position.add(this.velocity.multiply(dt)); 88 | 89 | this.wheelRotation += this.speed * dt * 0.1; 90 | 91 | this.bodyRoll = this.steering * Math.min(this.speed / this.maxSpeed, 1) * 0.2; 92 | this.bodyPitch = (this.throttle - this.brake * 0.5) * Math.min(this.speed / this.maxSpeed, 1) * 0.1; 93 | } 94 | 95 | updateVisuals(deltaTime) { 96 | // Update visual effects based on physics 97 | } 98 | 99 | reset() { 100 | this.position = new Vector3(0, 0.5, 0); 101 | this.velocity = new Vector3(0, 0, 0); 102 | this.speed = 0; 103 | this.forward = new Vector3(0, 0, 1); 104 | this.right = new Vector3(1, 0, 0); 105 | this.steering = 0; 106 | this.throttle = 0; 107 | this.brake = 0; 108 | this.wheelRotation = 0; 109 | this.bodyRoll = 0; 110 | this.bodyPitch = 0; 111 | } 112 | 113 | getCorners() { 114 | const halfWidth = this.width / 2; 115 | const halfLength = this.length / 2; 116 | 117 | const corners = [ 118 | new Vector3(-halfWidth, 0, -halfLength), // Front-left 119 | new Vector3(halfWidth, 0, -halfLength), // Front-right 120 | new Vector3(halfWidth, 0, halfLength), // Rear-right 121 | new Vector3(-halfWidth, 0, halfLength) // Rear-left 122 | ]; 123 | 124 | return corners.map(corner => { 125 | const rotated = new Vector3( 126 | corner.x * this.right.x + corner.z * this.forward.x, 127 | corner.y + this.bodyPitch * corner.z - this.bodyRoll * corner.x, 128 | corner.x * this.right.z + corner.z * this.forward.z 129 | ); 130 | return rotated.add(this.position); 131 | }); 132 | } 133 | 134 | getSpeedKMH() { 135 | return this.speed * 3.6; 136 | } 137 | } -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # mini Kode Agent 2 | 3 | This repository showcases a step-by-step recreation of a “mini Kode” workflow. With a few hundred lines of Python we rebuild the essential loops behind Anthropic’s engineering assistant and release them in two major stages: 4 | 5 | - **v1 (baseline)** – demonstrates the core *model-as-agent* loop: the LLM is the only decision maker, the CLI just exposes tools for reading, editing, writing files, and running shell commands. 6 | - **v2 (todos)** – layers structured planning on top of v1 with a shared todo board and system reminders so the model stays disciplined during multi-step tasks. 7 | 8 | Below is a detailed walkthrough of how each version operates and what changed between them. 9 | 10 | --- 11 | 12 | ## v1: Minimal “Model as Agent” Loop 13 | 14 | The first version proves a simple principle: **code supplies tools, the model drives the work**. About 400 lines of Python cover the following pillars: 15 | 16 | ### 1. System prompt guardrails 17 | - The `SYSTEM` string reminds the model that it lives inside the repository, must act through tools, and should summarize when finished. 18 | - This keeps multi-turn conversations action oriented instead of drifting into idle chat. 19 | 20 | ### 2. Unified tool dispatch 21 | - The CLI exposes four tools: `bash`, `read_file`, `write_file`, and `edit_text`. 22 | - When the model outputs a `tool_use` block, the dispatcher runs the corresponding helper and returns a `tool_result` block with truncated, colorized output. 23 | - Safety checks gate the tools (path validation, banned commands, output clamping) to prevent runaway actions. 24 | 25 | ### 3. Terminal experience 26 | - A background `Spinner` thread indicates model latency. 27 | - `pretty_tool_line` and `pretty_sub_line` format every tool call in a readable, ANSI-colored layout. 28 | - The full conversation is stored in `messages`, preserving context across tool invocations. 29 | 30 | ### 4. Main event loop 31 | - The CLI prints the current workspace, accepts user input, and appends it to history. 32 | - Each turn calls `client.messages.create`; if the model wants tools, the loop recursively executes them until plain text is returned. 33 | - Errors are caught by `log_error_debug` so the session doesn’t crash. 34 | 35 | > **Key insight:** a stable tool shell plus a focused system prompt is enough to let the model behave like a real coding agent. UI polish is optional; tool access is everything. 36 | 37 | --- 38 | 39 | ## v2: Structured Planning and System Reminders 40 | 41 | Version 2 answers a natural question: *how do we keep the model organized over longer tasks?* The upgrade introduces a todo board, a reminder mechanism, and English-only copy to keep the workflow predictable. 42 | 43 | ### 1. Todo toolchain 44 | - **`TodoManager`** maintains up to 20 entries, guarantees at most one `in_progress` item, and validates IDs, statuses, and descriptions. 45 | - **`TodoWrite`** becomes a first-class tool. The model calls it to create/update/complete todos; the CLI immediately renders colored status lines plus summary stats. 46 | - **Status colors** use a consistent palette: grey for pending, blue for in progress, green with strikethrough for completed. 47 | 48 | ### 2. System reminders 49 | - **Initial reminder:** before the first user message, a system block instructs the model to manage multi-step work through the todo board. 50 | - **10-turn reminder:** if ten consecutive turns pass without a todo update, another reminder block is injected to nudge the model back to structured planning. 51 | - **Auto reset:** every todo update resets the counter so reminders only trigger when discipline lapses. 52 | 53 | ### 3. Interaction changes 54 | - On input, user text and any pending reminders are bundled into a single content list so the model sees consistent context. 55 | - On output, todo results are printed immediately and appended to history, giving the model a short-term memory of its own plan. 56 | - All messages, summaries, and errors are now in English to match the neutral CLI aesthetic. 57 | 58 | ### 4. Benefits 59 | - **Structured guardrails:** the model has to plan before acting, reducing “winging it” behavior. 60 | - **Self-supervision:** the todo board is an external memory surface that keeps current priorities visible. 61 | - **Auditability:** the session transcript contains every todo change, which makes reviews and debugging easier. 62 | 63 | > **Looking for a production-ready toolkit?** Check out [Kode](https://github.com/shareAI-lab/Kode), the open-source Claude Code implementation we maintain. It adds Windows Bash support, WebSearch/WebFetch, Docker adapters, and IDE plugins on top of the ideas explored here. 64 | 65 | --- 66 | 67 | ## Summary 68 | 69 | - **v1 philosophy:** prove the minimal model-as-agent loop—tools are thin wrappers, the LLM carries the project. 70 | - **v2 philosophy:** enforce explicit planning via todos and system reminders so the workflow stays organized and transparent. 71 | 72 | Future iterations will experiment with sub-agent tasks, richer reminder matrices, and will keep backporting mature features from Kode into this learning-friendly codebase. Contributions and experiments are welcome! 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[codz] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | #poetry.toml 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 114 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 115 | #pdm.lock 116 | #pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # pixi 121 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 122 | #pixi.lock 123 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 124 | # in the .venv directory. It is recommended not to include this directory in version control. 125 | .pixi 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .envrc 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # pytype static type analyzer 166 | .pytype/ 167 | 168 | # Cython debug symbols 169 | cython_debug/ 170 | 171 | # PyCharm 172 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 173 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 174 | # and can be added to the global gitignore or merged into this file. For a more nuclear 175 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 176 | #.idea/ 177 | 178 | # Abstra 179 | # Abstra is an AI-powered process automation framework. 180 | # Ignore directories containing user credentials, local state, and settings. 181 | # Learn more at https://abstra.io/docs 182 | .abstra/ 183 | 184 | # Visual Studio Code 185 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 186 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 187 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 188 | # you could uncomment the following to ignore the entire vscode folder 189 | # .vscode/ 190 | 191 | # Ruff stuff: 192 | .ruff_cache/ 193 | 194 | # PyPI configuration file 195 | .pypirc 196 | 197 | # Cursor 198 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 199 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 200 | # refer to https://docs.cursor.com/context/ignore-files 201 | .cursorignore 202 | .cursorindexingignore 203 | 204 | # Marimo 205 | marimo/_static/ 206 | marimo/_lsp/ 207 | __marimo__/ 208 | -------------------------------------------------------------------------------- /demo/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Arial', sans-serif; 9 | background: #000; 10 | color: #fff; 11 | overflow: hidden; 12 | } 13 | 14 | #gameContainer { 15 | position: relative; 16 | width: 100vw; 17 | height: 100vh; 18 | } 19 | 20 | #gameCanvas { 21 | display: block; 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | /* UI Styles */ 27 | #ui { 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | width: 100%; 32 | height: 100%; 33 | pointer-events: none; 34 | z-index: 10; 35 | } 36 | 37 | #speedometer { 38 | position: absolute; 39 | bottom: 30px; 40 | right: 30px; 41 | text-align: center; 42 | background: rgba(0, 0, 0, 0.7); 43 | padding: 20px; 44 | border-radius: 10px; 45 | border: 2px solid #00ff00; 46 | } 47 | 48 | #speed { 49 | font-size: 48px; 50 | font-weight: bold; 51 | color: #00ff00; 52 | text-shadow: 0 0 10px #00ff00; 53 | } 54 | 55 | #speedUnit { 56 | font-size: 14px; 57 | color: #aaa; 58 | margin-top: 5px; 59 | } 60 | 61 | #position { 62 | position: absolute; 63 | top: 30px; 64 | right: 30px; 65 | background: rgba(0, 0, 0, 0.7); 66 | padding: 15px; 67 | border-radius: 10px; 68 | border: 2px solid #ffff00; 69 | } 70 | 71 | #positionText { 72 | font-size: 18px; 73 | color: #ffff00; 74 | font-weight: bold; 75 | } 76 | 77 | #lapTime { 78 | position: absolute; 79 | top: 30px; 80 | left: 30px; 81 | background: rgba(0, 0, 0, 0.7); 82 | padding: 15px; 83 | border-radius: 10px; 84 | border: 2px solid #00ffff; 85 | } 86 | 87 | #currentLap, #bestLap { 88 | font-size: 16px; 89 | color: #00ffff; 90 | margin: 5px 0; 91 | } 92 | 93 | #minimap { 94 | position: absolute; 95 | bottom: 30px; 96 | left: 30px; 97 | background: rgba(0, 0, 0, 0.7); 98 | padding: 10px; 99 | border-radius: 10px; 100 | border: 2px solid #ff00ff; 101 | } 102 | 103 | #minimapCanvas { 104 | border: 1px solid #ff00ff; 105 | border-radius: 5px; 106 | } 107 | 108 | #controls { 109 | position: absolute; 110 | bottom: 30px; 111 | left: 50%; 112 | transform: translateX(-50%); 113 | background: rgba(0, 0, 0, 0.7); 114 | padding: 15px; 115 | border-radius: 10px; 116 | border: 2px solid #ff6600; 117 | display: flex; 118 | gap: 20px; 119 | } 120 | 121 | .control-item { 122 | font-size: 12px; 123 | color: #ff6600; 124 | text-align: center; 125 | } 126 | 127 | /* Menu Styles */ 128 | .menu { 129 | position: absolute; 130 | top: 0; 131 | left: 0; 132 | width: 100%; 133 | height: 100%; 134 | background: rgba(0, 0, 0, 0.9); 135 | display: flex; 136 | flex-direction: column; 137 | justify-content: center; 138 | align-items: center; 139 | z-index: 100; 140 | opacity: 0; 141 | visibility: hidden; 142 | transition: all 0.3s ease; 143 | } 144 | 145 | .menu.active { 146 | opacity: 1; 147 | visibility: visible; 148 | } 149 | 150 | .menu h1 { 151 | font-size: 48px; 152 | margin-bottom: 50px; 153 | text-shadow: 0 0 20px #00ff00; 154 | color: #00ff00; 155 | } 156 | 157 | .menu-buttons { 158 | display: flex; 159 | flex-direction: column; 160 | gap: 20px; 161 | } 162 | 163 | .menu-buttons button { 164 | padding: 15px 40px; 165 | font-size: 18px; 166 | background: linear-gradient(45deg, #00ff00, #00aa00); 167 | color: #000; 168 | border: none; 169 | border-radius: 5px; 170 | cursor: pointer; 171 | transition: all 0.3s ease; 172 | font-weight: bold; 173 | } 174 | 175 | .menu-buttons button:hover { 176 | background: linear-gradient(45deg, #00aa00, #00ff00); 177 | transform: scale(1.05); 178 | box-shadow: 0 0 20px #00ff00; 179 | } 180 | 181 | /* Loading Screen */ 182 | .loading { 183 | position: absolute; 184 | top: 0; 185 | left: 0; 186 | width: 100%; 187 | height: 100%; 188 | background: rgba(0, 0, 0, 0.95); 189 | display: flex; 190 | flex-direction: column; 191 | justify-content: center; 192 | align-items: center; 193 | z-index: 200; 194 | opacity: 0; 195 | visibility: hidden; 196 | transition: all 0.3s ease; 197 | } 198 | 199 | .loading.active { 200 | opacity: 1; 201 | visibility: visible; 202 | } 203 | 204 | .loading-text { 205 | font-size: 24px; 206 | margin-bottom: 20px; 207 | color: #00ff00; 208 | } 209 | 210 | .loading-bar { 211 | width: 300px; 212 | height: 20px; 213 | background: rgba(255, 255, 255, 0.2); 214 | border-radius: 10px; 215 | overflow: hidden; 216 | border: 1px solid #00ff00; 217 | } 218 | 219 | .loading-progress { 220 | height: 100%; 221 | background: linear-gradient(90deg, #00ff00, #00aa00); 222 | width: 0%; 223 | transition: width 0.3s ease; 224 | } 225 | 226 | /* Responsive Design */ 227 | @media (max-width: 768px) { 228 | #speedometer { 229 | bottom: 20px; 230 | right: 20px; 231 | padding: 15px; 232 | } 233 | 234 | #speed { 235 | font-size: 36px; 236 | } 237 | 238 | #position, #lapTime { 239 | top: 20px; 240 | padding: 10px; 241 | } 242 | 243 | #minimap { 244 | bottom: 20px; 245 | left: 20px; 246 | } 247 | 248 | #controls { 249 | bottom: 20px; 250 | flex-direction: column; 251 | gap: 10px; 252 | padding: 10px; 253 | } 254 | 255 | .menu h1 { 256 | font-size: 36px; 257 | } 258 | 259 | .menu-buttons button { 260 | padding: 12px 30px; 261 | font-size: 16px; 262 | } 263 | } -------------------------------------------------------------------------------- /demo/js/track.js: -------------------------------------------------------------------------------- 1 | class Track { 2 | constructor() { 3 | this.width = 12; 4 | this.segments = []; 5 | this.barriers = []; 6 | this.checkpoints = []; 7 | this.startLine = null; 8 | 9 | this.generateTrack(); 10 | this.generateBarriers(); 11 | this.generateCheckpoints(); 12 | } 13 | 14 | generateTrack() { 15 | const trackPoints = [ 16 | new Vector3(0, 0, 0), 17 | new Vector3(0, 0, 50), 18 | new Vector3(30, 0, 80), 19 | new Vector3(60, 0, 80), 20 | new Vector3(90, 0, 50), 21 | new Vector3(90, 0, 0), 22 | new Vector3(60, 0, -30), 23 | new Vector3(30, 0, -30), 24 | new Vector3(0, 0, 0) 25 | ]; 26 | 27 | for (let i = 0; i < trackPoints.length - 1; i++) { 28 | const start = trackPoints[i]; 29 | const end = trackPoints[i + 1]; 30 | const direction = end.subtract(start).normalize(); 31 | const length = start.distance(end); 32 | 33 | this.segments.push({ 34 | start: start, 35 | end: end, 36 | direction: direction, 37 | length: length, 38 | width: this.width 39 | }); 40 | } 41 | 42 | this.startLine = { 43 | position: new Vector3(0, 0, -5), 44 | direction: new Vector3(0, 0, 1), 45 | width: this.width 46 | }; 47 | } 48 | 49 | generateBarriers() { 50 | this.barriers = []; 51 | 52 | for (const segment of this.segments) { 53 | const leftBarrier = this.createBarrier(segment, -this.width / 2 - 1); 54 | const rightBarrier = this.createBarrier(segment, this.width / 2 + 1); 55 | 56 | this.barriers.push(leftBarrier, rightBarrier); 57 | } 58 | } 59 | 60 | createBarrier(segment, offset) { 61 | const perpendicular = new Vector3(-segment.direction.z, 0, segment.direction.x); 62 | const start = segment.start.add(perpendicular.multiply(offset)); 63 | const end = segment.end.add(perpendicular.multiply(offset)); 64 | 65 | return { 66 | start: start, 67 | end: end, 68 | height: 2, 69 | width: 0.5 70 | }; 71 | } 72 | 73 | generateCheckpoints() { 74 | this.checkpoints = []; 75 | 76 | for (let i = 0; i < this.segments.length; i++) { 77 | const segment = this.segments[i]; 78 | const midpoint = segment.start.add(segment.end).divide(2); 79 | 80 | this.checkpoints.push({ 81 | position: midpoint, 82 | width: this.width, 83 | passed: false, 84 | index: i 85 | }); 86 | } 87 | } 88 | 89 | getTrackBounds() { 90 | let minX = Infinity, maxX = -Infinity; 91 | let minZ = Infinity, maxZ = -Infinity; 92 | 93 | for (const segment of this.segments) { 94 | minX = Math.min(minX, segment.start.x, segment.end.x); 95 | maxX = Math.max(maxX, segment.start.x, segment.end.x); 96 | minZ = Math.min(minZ, segment.start.z, segment.end.z); 97 | maxZ = Math.max(maxZ, segment.start.z, segment.end.z); 98 | } 99 | 100 | return { minX, maxX, minZ, maxZ }; 101 | } 102 | 103 | getTrackWidthAt(position) { 104 | return this.width; 105 | } 106 | 107 | getNearestTrackPoint(position) { 108 | let nearestPoint = null; 109 | let minDistance = Infinity; 110 | 111 | for (const segment of this.segments) { 112 | const point = this.getClosestPointOnSegment(position, segment); 113 | const distance = position.distance(point); 114 | 115 | if (distance < minDistance) { 116 | minDistance = distance; 117 | nearestPoint = point; 118 | } 119 | } 120 | 121 | return { point: nearestPoint, distance: minDistance }; 122 | } 123 | 124 | getClosestPointOnSegment(point, segment) { 125 | const toStart = point.subtract(segment.start); 126 | const toEnd = segment.end.subtract(segment.start); 127 | 128 | const t = Math.max(0, Math.min(1, toStart.dot(toEnd) / toEnd.dot(toEnd))); 129 | 130 | return segment.start.add(toEnd.multiply(t)); 131 | } 132 | 133 | isOnTrack(position) { 134 | const trackInfo = this.getNearestTrackPoint(position); 135 | return trackInfo.distance <= this.width / 2; 136 | } 137 | 138 | checkCollision(position, radius = 1) { 139 | for (const barrier of this.barriers) { 140 | if (this.checkBarrierCollision(position, barrier, radius)) { 141 | return true; 142 | } 143 | } 144 | return false; 145 | } 146 | 147 | checkBarrierCollision(position, barrier, radius) { 148 | const closestPoint = this.getClosestPointOnSegment(position, { 149 | start: barrier.start, 150 | end: barrier.end 151 | }); 152 | 153 | return position.distance(closestPoint) < radius + barrier.width; 154 | } 155 | 156 | resetCheckpoints() { 157 | for (const checkpoint of this.checkpoints) { 158 | checkpoint.passed = false; 159 | } 160 | } 161 | 162 | checkLapComplete(carPosition) { 163 | if (!this.startLine) return false; 164 | 165 | const distanceToStart = carPosition.distance(this.startLine.position); 166 | 167 | if (distanceToStart < 5) { 168 | const allPassed = this.checkpoints.every(cp => cp.passed); 169 | 170 | if (allPassed) { 171 | this.resetCheckpoints(); 172 | return true; 173 | } 174 | } 175 | 176 | return false; 177 | } 178 | 179 | updateCheckpoints(carPosition) { 180 | for (const checkpoint of this.checkpoints) { 181 | if (!checkpoint.passed) { 182 | const distance = carPosition.distance(checkpoint.position); 183 | if (distance < 8) { 184 | checkpoint.passed = true; 185 | } 186 | } 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /demo/js/utils.js: -------------------------------------------------------------------------------- 1 | // Utility functions for the racing game 2 | 3 | class Vector3 { 4 | constructor(x = 0, y = 0, z = 0) { 5 | this.x = x; 6 | this.y = y; 7 | this.z = z; 8 | } 9 | 10 | clone() { 11 | return new Vector3(this.x, this.y, this.z); 12 | } 13 | 14 | add(v) { 15 | return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z); 16 | } 17 | 18 | subtract(v) { 19 | return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); 20 | } 21 | 22 | multiply(scalar) { 23 | return new Vector3(this.x * scalar, this.y * scalar, this.z * scalar); 24 | } 25 | 26 | length() { 27 | return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); 28 | } 29 | 30 | normalize() { 31 | const len = this.length(); 32 | if (len === 0) return new Vector3(); 33 | return new Vector3(this.x / len, this.y / len, this.z / len); 34 | } 35 | 36 | dot(v) { 37 | return this.x * v.x + this.y * v.y + this.z * v.z; 38 | } 39 | 40 | cross(v) { 41 | return new Vector3( 42 | this.y * v.z - this.z * v.y, 43 | this.z * v.x - this.x * v.z, 44 | this.x * v.y - this.y * v.x 45 | ); 46 | } 47 | } 48 | 49 | class MathUtils { 50 | static lerp(a, b, t) { 51 | return a + (b - a) * t; 52 | } 53 | 54 | static clamp(value, min, max) { 55 | return Math.max(min, Math.min(max, value)); 56 | } 57 | 58 | static randomRange(min, max) { 59 | return Math.random() * (max - min) + min; 60 | } 61 | 62 | static degToRad(degrees) { 63 | return degrees * (Math.PI / 180); 64 | } 65 | 66 | static radToDeg(radians) { 67 | return radians * (180 / Math.PI); 68 | } 69 | 70 | static smoothstep(min, max, value) { 71 | const t = MathUtils.clamp((value - min) / (max - min), 0, 1); 72 | return t * t * (3 - 2 * t); 73 | } 74 | } 75 | 76 | class Color { 77 | constructor(r = 1, g = 1, b = 1) { 78 | this.r = r; 79 | this.g = g; 80 | this.b = b; 81 | } 82 | 83 | toHex() { 84 | const r = Math.floor(this.r * 255); 85 | const g = Math.floor(this.g * 255); 86 | const b = Math.floor(this.b * 255); 87 | return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; 88 | } 89 | 90 | toString() { 91 | return `rgb(${Math.floor(this.r * 255)}, ${Math.floor(this.g * 255)}, ${Math.floor(this.b * 255)})`; 92 | } 93 | } 94 | 95 | // Input handling 96 | class InputManager { 97 | constructor() { 98 | this.keys = {}; 99 | this.mouse = { x: 0, y: 0, buttons: {} }; 100 | this.gamepad = null; 101 | 102 | this.setupEventListeners(); 103 | } 104 | 105 | setupEventListeners() { 106 | document.addEventListener('keydown', (e) => { 107 | this.keys[e.code] = true; 108 | }); 109 | 110 | document.addEventListener('keyup', (e) => { 111 | this.keys[e.code] = false; 112 | }); 113 | 114 | document.addEventListener('mousemove', (e) => { 115 | this.mouse.x = e.clientX; 116 | this.mouse.y = e.clientY; 117 | }); 118 | 119 | document.addEventListener('mousedown', (e) => { 120 | this.mouse.buttons[e.button] = true; 121 | }); 122 | 123 | document.addEventListener('mouseup', (e) => { 124 | this.mouse.buttons[e.button] = false; 125 | }); 126 | 127 | window.addEventListener('gamepadconnected', (e) => { 128 | this.gamepad = e.gamepad; 129 | }); 130 | 131 | window.addEventListener('gamepaddisconnected', (e) => { 132 | if (this.gamepad && this.gamepad.index === e.gamepad.index) { 133 | this.gamepad = null; 134 | } 135 | }); 136 | } 137 | 138 | isKeyPressed(key) { 139 | return !!this.keys[key]; 140 | } 141 | 142 | getMovementInput() { 143 | return { 144 | forward: this.isKeyPressed('KeyW') || this.isKeyPressed('ArrowUp'), 145 | backward: this.isKeyPressed('KeyS') || this.isKeyPressed('ArrowDown'), 146 | left: this.isKeyPressed('KeyA') || this.isKeyPressed('ArrowLeft'), 147 | right: this.isKeyPressed('KeyD') || this.isKeyPressed('ArrowRight'), 148 | brake: this.isKeyPressed('Space'), 149 | boost: this.isKeyPressed('ShiftLeft') || this.isKeyPressed('ShiftRight') 150 | }; 151 | } 152 | 153 | updateGamepad() { 154 | if (this.gamepad) { 155 | const gamepads = navigator.getGamepads(); 156 | this.gamepad = gamepads[this.gamepad.index]; 157 | } 158 | } 159 | } 160 | 161 | // Performance monitoring 162 | class PerformanceMonitor { 163 | constructor() { 164 | this.fps = 0; 165 | this.frameTime = 0; 166 | this.lastTime = performance.now(); 167 | this.frameCount = 0; 168 | this.fpsUpdateTime = 0; 169 | } 170 | 171 | update() { 172 | const currentTime = performance.now(); 173 | this.frameTime = currentTime - this.lastTime; 174 | this.lastTime = currentTime; 175 | 176 | this.frameCount++; 177 | this.fpsUpdateTime += this.frameTime; 178 | 179 | if (this.fpsUpdateTime >= 1000) { 180 | this.fps = Math.round(this.frameCount * 1000 / this.fpsUpdateTime); 181 | this.frameCount = 0; 182 | this.fpsUpdateTime = 0; 183 | } 184 | } 185 | } 186 | 187 | // Resource loading 188 | class ResourceManager { 189 | constructor() { 190 | this.textures = new Map(); 191 | this.models = new Map(); 192 | this.sounds = new Map(); 193 | this.loadingProgress = 0; 194 | this.totalResources = 0; 195 | this.loadedResources = 0; 196 | } 197 | 198 | async loadTexture(name, url) { 199 | return new Promise((resolve, reject) => { 200 | const texture = new THREE.TextureLoader().load( 201 | url, 202 | () => { 203 | this.textures.set(name, texture); 204 | this.onResourceLoaded(); 205 | resolve(texture); 206 | }, 207 | undefined, 208 | reject 209 | ); 210 | }); 211 | } 212 | 213 | async loadModel(name, url) { 214 | return new Promise((resolve, reject) => { 215 | const loader = new THREE.GLTFLoader(); 216 | loader.load( 217 | url, 218 | (gltf) => { 219 | this.models.set(name, gltf); 220 | this.onResourceLoaded(); 221 | resolve(gltf); 222 | }, 223 | undefined, 224 | reject 225 | ); 226 | }); 227 | } 228 | 229 | async loadSound(name, url) { 230 | return new Promise((resolve, reject) => { 231 | const audio = new Audio(url); 232 | audio.addEventListener('canplaythrough', () => { 233 | this.sounds.set(name, audio); 234 | this.onResourceLoaded(); 235 | resolve(audio); 236 | }); 237 | audio.addEventListener('error', reject); 238 | }); 239 | } 240 | 241 | onResourceLoaded() { 242 | this.loadedResources++; 243 | this.loadingProgress = this.loadedResources / this.totalResources; 244 | } 245 | 246 | getTexture(name) { 247 | return this.textures.get(name); 248 | } 249 | 250 | getModel(name) { 251 | return this.models.get(name); 252 | } 253 | 254 | getSound(name) { 255 | return this.sounds.get(name); 256 | } 257 | } 258 | 259 | // Export for use in other modules 260 | if (typeof module !== 'undefined' && module.exports) { 261 | module.exports = { 262 | Vector3, 263 | MathUtils, 264 | Color, 265 | InputManager, 266 | PerformanceMonitor, 267 | ResourceManager 268 | }; 269 | } -------------------------------------------------------------------------------- /demo/catch_game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Catch the Falling Objects 7 | 68 | 69 | 70 |
71 |

Catch the Falling Objects!

72 |
Use arrow keys or A/D to move the basket
73 |
Score: 0
74 | 75 |
Game Over! Final Score: 0
76 | 77 | 78 |
79 | 80 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /demo/js/game.js: -------------------------------------------------------------------------------- 1 | class Game { 2 | constructor() { 3 | this.canvas = document.getElementById('gameCanvas'); 4 | this.renderer = new Renderer(this.canvas); 5 | this.car = new Car(); 6 | this.track = new Track(); 7 | 8 | this.input = { 9 | forward: false, 10 | backward: false, 11 | left: false, 12 | right: false, 13 | handbrake: false, 14 | reset: false 15 | }; 16 | 17 | this.gameState = 'racing'; // racing, paused, finished 18 | this.lapTime = 0; 19 | this.bestLap = null; 20 | this.currentLapStart = 0; 21 | this.raceStartTime = 0; 22 | 23 | this.lastTime = 0; 24 | this.deltaTime = 0; 25 | this.fps = 0; 26 | this.frameCount = 0; 27 | this.fpsUpdateTime = 0; 28 | 29 | this.setupEventListeners(); 30 | this.resize(); 31 | this.start(); 32 | } 33 | 34 | setupEventListeners() { 35 | // Keyboard controls 36 | document.addEventListener('keydown', (e) => { 37 | switch(e.code) { 38 | case 'KeyW': 39 | case 'ArrowUp': 40 | this.input.forward = true; 41 | e.preventDefault(); 42 | break; 43 | case 'KeyS': 44 | case 'ArrowDown': 45 | this.input.backward = true; 46 | e.preventDefault(); 47 | break; 48 | case 'KeyA': 49 | case 'ArrowLeft': 50 | this.input.left = true; 51 | e.preventDefault(); 52 | break; 53 | case 'KeyD': 54 | case 'ArrowRight': 55 | this.input.right = true; 56 | e.preventDefault(); 57 | break; 58 | case 'Space': 59 | this.input.handbrake = true; 60 | e.preventDefault(); 61 | break; 62 | case 'KeyR': 63 | this.input.reset = true; 64 | e.preventDefault(); 65 | break; 66 | case 'KeyP': 67 | this.togglePause(); 68 | e.preventDefault(); 69 | break; 70 | case 'KeyF': 71 | this.renderer.toggleDebug(); 72 | e.preventDefault(); 73 | break; 74 | case 'KeyT': 75 | this.renderer.toggleWireframe(); 76 | e.preventDefault(); 77 | break; 78 | } 79 | }); 80 | 81 | document.addEventListener('keyup', (e) => { 82 | switch(e.code) { 83 | case 'KeyW': 84 | case 'ArrowUp': 85 | this.input.forward = false; 86 | break; 87 | case 'KeyS': 88 | case 'ArrowDown': 89 | this.input.backward = false; 90 | break; 91 | case 'KeyA': 92 | case 'ArrowLeft': 93 | this.input.left = false; 94 | break; 95 | case 'KeyD': 96 | case 'ArrowRight': 97 | this.input.right = false; 98 | break; 99 | case 'Space': 100 | this.input.handbrake = false; 101 | break; 102 | case 'KeyR': 103 | this.input.reset = false; 104 | break; 105 | } 106 | }); 107 | 108 | // Window resize 109 | window.addEventListener('resize', () => this.resize()); 110 | 111 | // Prevent context menu on canvas 112 | this.canvas.addEventListener('contextmenu', (e) => e.preventDefault()); 113 | } 114 | 115 | resize() { 116 | const width = window.innerWidth; 117 | const height = window.innerHeight; 118 | 119 | this.canvas.width = width; 120 | this.canvas.height = height; 121 | this.renderer.setSize(width, height); 122 | } 123 | 124 | start() { 125 | this.raceStartTime = performance.now(); 126 | this.currentLapStart = this.raceStartTime; 127 | this.gameState = 'racing'; 128 | this.lastTime = performance.now(); 129 | 130 | this.gameLoop(); 131 | } 132 | 133 | gameLoop(currentTime = performance.now()) { 134 | this.deltaTime = (currentTime - this.lastTime) / 1000; 135 | this.lastTime = currentTime; 136 | 137 | // Update FPS 138 | this.frameCount++; 139 | if (currentTime - this.fpsUpdateTime > 1000) { 140 | this.fps = this.frameCount; 141 | this.frameCount = 0; 142 | this.fpsUpdateTime = currentTime; 143 | } 144 | 145 | if (this.gameState === 'racing') { 146 | this.update(this.deltaTime); 147 | } 148 | 149 | this.render(); 150 | 151 | requestAnimationFrame((time) => this.gameLoop(time)); 152 | } 153 | 154 | update(deltaTime) { 155 | // Update car 156 | this.car.update(deltaTime, this.input); 157 | 158 | // Handle track collisions 159 | if (this.track.checkCollision(this.car.position)) { 160 | // Simple collision response - reduce speed 161 | this.car.speed *= 0.5; 162 | } 163 | 164 | // Update lap timing 165 | this.updateLapTiming(); 166 | 167 | // Update checkpoints 168 | this.track.updateCheckpoints(this.car.position); 169 | 170 | // Check for lap completion 171 | if (this.track.checkLapComplete(this.car.position)) { 172 | this.completeLap(); 173 | } 174 | } 175 | 176 | updateLapTiming() { 177 | const currentTime = performance.now(); 178 | this.lapTime = (currentTime - this.currentLapStart) / 1000; 179 | 180 | // Update UI 181 | const minutes = Math.floor(this.lapTime / 60); 182 | const seconds = Math.floor(this.lapTime % 60); 183 | const milliseconds = Math.floor((this.lapTime % 1) * 100); 184 | 185 | const lapTimeElement = document.getElementById('lapTime'); 186 | if (lapTimeElement) { 187 | lapTimeElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`; 188 | } 189 | 190 | // Update best lap 191 | if (this.bestLap !== null) { 192 | const bestMinutes = Math.floor(this.bestLap / 60); 193 | const bestSeconds = Math.floor(this.bestLap % 60); 194 | const bestMilliseconds = Math.floor((this.bestLap % 1) * 100); 195 | 196 | const bestLapElement = document.getElementById('bestLap'); 197 | if (bestLapElement) { 198 | bestLapElement.textContent = `${bestMinutes}:${bestSeconds.toString().padStart(2, '0')}.${bestMilliseconds.toString().padStart(2, '0')}`; 199 | } 200 | } 201 | } 202 | 203 | completeLap() { 204 | const lapTime = this.lapTime; 205 | 206 | if (this.bestLap === null || lapTime < this.bestLap) { 207 | this.bestLap = lapTime; 208 | } 209 | 210 | this.currentLapStart = performance.now(); 211 | 212 | // Reset checkpoints for next lap 213 | this.track.resetCheckpoints(); 214 | 215 | console.log(`Lap completed: ${lapTime.toFixed(2)}s`); 216 | } 217 | 218 | render() { 219 | this.renderer.render(this.car, this.track); 220 | } 221 | 222 | togglePause() { 223 | if (this.gameState === 'racing') { 224 | this.gameState = 'paused'; 225 | } else if (this.gameState === 'paused') { 226 | this.gameState = 'racing'; 227 | } 228 | } 229 | 230 | reset() { 231 | this.car.reset(); 232 | this.track.resetCheckpoints(); 233 | this.lapTime = 0; 234 | this.currentLapStart = performance.now(); 235 | this.gameState = 'racing'; 236 | } 237 | 238 | getGameInfo() { 239 | return { 240 | speed: this.car.getSpeedKMH(), 241 | lapTime: this.lapTime, 242 | bestLap: this.bestLap, 243 | fps: this.fps, 244 | onTrack: this.track.isOnTrack(this.car.position), 245 | gameState: this.gameState 246 | }; 247 | } 248 | } 249 | 250 | // Initialize game when page loads 251 | window.addEventListener('load', () => { 252 | const game = new Game(); 253 | 254 | // Make game accessible for debugging 255 | window.game = game; 256 | 257 | console.log('3D Racing Game loaded!'); 258 | console.log('Controls:'); 259 | console.log('- WASD or Arrow Keys: Drive'); 260 | console.log('- Space: Handbrake'); 261 | console.log('- R: Reset car'); 262 | console.log('- P: Pause/Resume'); 263 | console.log('- F: Toggle debug info'); 264 | console.log('- T: Toggle wireframe mode'); 265 | }); -------------------------------------------------------------------------------- /demo/racing_game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Racing Game 7 | 78 | 79 | 80 |
81 | 82 |
83 |
Score: 0
84 |
Speed: 0 km/h
85 |
86 |
87 | Use Arrow Keys to steer, Up Arrow to accelerate, Down Arrow to brake 88 |
89 |
90 |
Game Over!
91 |
Final Score: 0
92 | 93 |
94 |
95 | 96 | 306 | 307 | -------------------------------------------------------------------------------- /demo/js/renderer.js: -------------------------------------------------------------------------------- 1 | class Renderer { 2 | constructor(canvas) { 3 | this.canvas = canvas; 4 | this.ctx = canvas.getContext('2d'); 5 | this.width = canvas.width; 6 | this.height = canvas.height; 7 | 8 | this.camera = new Camera(60, this.width / this.height, 0.1, 1000); 9 | this.camera.position = new Vector3(0, 15, -20); 10 | 11 | this.debugMode = false; 12 | this.wireframe = false; 13 | } 14 | 15 | setSize(width, height) { 16 | this.canvas.width = width; 17 | this.canvas.height = height; 18 | this.width = width; 19 | this.height = height; 20 | this.camera.setAspectRatio(width / height); 21 | } 22 | 23 | clear() { 24 | this.ctx.fillStyle = '#87CEEB'; // Sky blue 25 | this.ctx.fillRect(0, 0, this.width, this.height); 26 | 27 | // Draw ground 28 | this.ctx.fillStyle = '#228B22'; // Forest green 29 | this.ctx.fillRect(0, this.height * 0.7, this.width, this.height * 0.3); 30 | } 31 | 32 | render(car, track) { 33 | this.clear(); 34 | 35 | // Update camera to follow car 36 | this.camera.followCar(car); 37 | 38 | // Render track 39 | this.renderTrack(track); 40 | 41 | // Render car 42 | this.renderCar(car); 43 | 44 | // Render UI elements 45 | this.renderUI(car, track); 46 | 47 | if (this.debugMode) { 48 | this.renderDebugInfo(car, track); 49 | } 50 | } 51 | 52 | renderTrack(track) { 53 | // Render track surface 54 | for (const segment of track.segments) { 55 | this.renderTrackSegment(segment); 56 | } 57 | 58 | // Render barriers 59 | for (const barrier of track.barriers) { 60 | this.renderBarrier(barrier); 61 | } 62 | 63 | // Render checkpoints 64 | for (const checkpoint of track.checkpoints) { 65 | if (!checkpoint.passed) { 66 | this.renderCheckpoint(checkpoint); 67 | } 68 | } 69 | 70 | // Render start line 71 | if (track.startLine) { 72 | this.renderStartLine(track.startLine); 73 | } 74 | } 75 | 76 | renderTrackSegment(segment) { 77 | const perpendicular = new Vector3(-segment.direction.z, 0, segment.direction.x); 78 | const halfWidth = segment.width / 2; 79 | 80 | const corners = [ 81 | segment.start.add(perpendicular.multiply(-halfWidth)), 82 | segment.start.add(perpendicular.multiply(halfWidth)), 83 | segment.end.add(perpendicular.multiply(halfWidth)), 84 | segment.end.add(perpendicular.multiply(-halfWidth)) 85 | ]; 86 | 87 | const screenCorners = corners.map(corner => 88 | this.camera.worldToScreen(corner) 89 | ); 90 | 91 | // Only render if at least some corners are visible 92 | if (screenCorners.some(corner => corner.z > 0 && corner.z < 1)) { 93 | this.ctx.fillStyle = '#404040'; // Dark gray track 94 | this.ctx.strokeStyle = '#FFFF00'; // Yellow lines 95 | this.ctx.lineWidth = 2; 96 | 97 | this.ctx.beginPath(); 98 | this.ctx.moveTo(screenCorners[0].x * this.width, screenCorners[0].y * this.height); 99 | for (let i = 1; i < screenCorners.length; i++) { 100 | this.ctx.lineTo(screenCorners[i].x * this.width, screenCorners[i].y * this.height); 101 | } 102 | this.ctx.closePath(); 103 | 104 | if (this.wireframe) { 105 | this.ctx.stroke(); 106 | } else { 107 | this.ctx.fill(); 108 | this.ctx.stroke(); 109 | } 110 | } 111 | } 112 | 113 | renderBarrier(barrier) { 114 | const screenStart = this.camera.worldToScreen(barrier.start); 115 | const screenEnd = this.camera.worldToScreen(barrier.end); 116 | 117 | if (screenStart.z > 0 && screenStart.z < 1 && screenEnd.z > 0 && screenEnd.z < 1) { 118 | this.ctx.strokeStyle = '#FF0000'; // Red barriers 119 | this.ctx.lineWidth = 8; 120 | 121 | this.ctx.beginPath(); 122 | this.ctx.moveTo(screenStart.x * this.width, screenStart.y * this.height); 123 | this.ctx.lineTo(screenEnd.x * this.width, screenEnd.y * this.height); 124 | this.ctx.stroke(); 125 | } 126 | } 127 | 128 | renderCheckpoint(checkpoint) { 129 | const screenPos = this.camera.worldToScreen(checkpoint.position); 130 | 131 | if (screenPos.z > 0 && screenPos.z < 1) { 132 | const size = 20 / screenPos.z; // Size decreases with distance 133 | 134 | this.ctx.strokeStyle = '#00FF00'; // Green checkpoints 135 | this.ctx.lineWidth = 3; 136 | 137 | this.ctx.beginPath(); 138 | this.ctx.arc(screenPos.x * this.width, screenPos.y * this.height, size, 0, Math.PI * 2); 139 | this.ctx.stroke(); 140 | 141 | // Checkpoint number 142 | this.ctx.fillStyle = '#00FF00'; 143 | this.ctx.font = '12px Arial'; 144 | this.ctx.textAlign = 'center'; 145 | this.ctx.fillText((checkpoint.index + 1).toString(), screenPos.x * this.width, screenPos.y * this.height); 146 | } 147 | } 148 | 149 | renderStartLine(startLine) { 150 | const perpendicular = new Vector3(-startLine.direction.z, 0, startLine.direction.x); 151 | const halfWidth = startLine.width / 2; 152 | 153 | const start1 = startLine.position.add(perpendicular.multiply(-halfWidth)); 154 | const start2 = startLine.position.add(perpendicular.multiply(halfWidth)); 155 | 156 | const screen1 = this.camera.worldToScreen(start1); 157 | const screen2 = this.camera.worldToScreen(start2); 158 | 159 | if (screen1.z > 0 && screen1.z < 1 && screen2.z > 0 && screen2.z < 1) { 160 | this.ctx.strokeStyle = '#FFFFFF'; // White start line 161 | this.ctx.lineWidth = 4; 162 | 163 | this.ctx.beginPath(); 164 | this.ctx.moveTo(screen1.x * this.width, screen1.y * this.height); 165 | this.ctx.lineTo(screen2.x * this.width, screen2.y * this.height); 166 | this.ctx.stroke(); 167 | } 168 | } 169 | 170 | renderCar(car) { 171 | const corners = car.getCorners(); 172 | const screenCorners = corners.map(corner => 173 | this.camera.worldToScreen(corner) 174 | ); 175 | 176 | // Only render if visible 177 | if (screenCorners.some(corner => corner.z > 0 && corner.z < 1)) { 178 | // Car body 179 | this.ctx.fillStyle = car.color; 180 | this.ctx.strokeStyle = '#000000'; 181 | this.ctx.lineWidth = 1; 182 | 183 | this.ctx.beginPath(); 184 | this.ctx.moveTo(screenCorners[0].x * this.width, screenCorners[0].y * this.height); 185 | for (let i = 1; i < screenCorners.length; i++) { 186 | this.ctx.lineTo(screenCorners[i].x * this.width, screenCorners[i].y * this.height); 187 | } 188 | this.ctx.closePath(); 189 | 190 | if (this.wireframe) { 191 | this.ctx.stroke(); 192 | } else { 193 | this.ctx.fill(); 194 | this.ctx.stroke(); 195 | } 196 | 197 | // Wheels (simplified as circles) 198 | this.renderWheels(car); 199 | 200 | // Direction indicator 201 | this.renderDirectionIndicator(car); 202 | } 203 | } 204 | 205 | renderWheels(car) { 206 | const wheelPositions = [ 207 | car.position.add(car.right.multiply(-1.2)).add(car.forward.multiply(-1.5)), 208 | car.position.add(car.right.multiply(1.2)).add(car.forward.multiply(-1.5)), 209 | car.position.add(car.right.multiply(-1.2)).add(car.forward.multiply(1.5)), 210 | car.position.add(car.right.multiply(1.2)).add(car.forward.multiply(1.5)) 211 | ]; 212 | 213 | this.ctx.fillStyle = '#202020'; 214 | 215 | for (const wheelPos of wheelPositions) { 216 | const screenWheel = this.camera.worldToScreen(wheelPos); 217 | 218 | if (screenWheel.z > 0 && screenWheel.z < 1) { 219 | const size = 8 / screenWheel.z; 220 | 221 | this.ctx.beginPath(); 222 | this.ctx.arc(screenWheel.x * this.width, screenWheel.y * this.height, size, 0, Math.PI * 2); 223 | this.ctx.fill(); 224 | } 225 | } 226 | } 227 | 228 | renderDirectionIndicator(car) { 229 | const frontPos = car.position.add(car.forward.multiply(3)); 230 | const screenFront = this.camera.worldToScreen(frontPos); 231 | const screenCar = this.camera.worldToScreen(car.position); 232 | 233 | if (screenFront.z > 0 && screenCar.z > 0) { 234 | this.ctx.strokeStyle = '#FFFF00'; 235 | this.ctx.lineWidth = 2; 236 | 237 | this.ctx.beginPath(); 238 | this.ctx.moveTo(screenCar.x * this.width, screenCar.y * this.height); 239 | this.ctx.lineTo(screenFront.x * this.width, screenFront.y * this.height); 240 | this.ctx.stroke(); 241 | } 242 | } 243 | 244 | renderUI(car, track) { 245 | // Speed indicator 246 | const speed = car.getSpeedKMH(); 247 | const speedElement = document.getElementById('speed'); 248 | if (speedElement) { 249 | speedElement.textContent = Math.round(speed); 250 | } 251 | } 252 | 253 | renderDebugInfo(car, track) { 254 | this.ctx.fillStyle = '#FFFFFF'; 255 | this.ctx.font = '10px monospace'; 256 | this.ctx.textAlign = 'left'; 257 | 258 | const debugInfo = [ 259 | `Position: ${car.position.toString()}`, 260 | `Speed: ${car.getSpeedKMH().toFixed(1)} km/h`, 261 | `On Track: ${track.isOnTrack(car.position)}`, 262 | `Camera: ${this.camera.position.toString()}`, 263 | `FPS: ${Math.round(1000 / 16)}` // Placeholder 264 | ]; 265 | 266 | for (let i = 0; i < debugInfo.length; i++) { 267 | this.ctx.fillText(debugInfo[i], 10, 20 + i * 12); 268 | } 269 | } 270 | 271 | toggleDebug() { 272 | this.debugMode = !this.debugMode; 273 | } 274 | 275 | toggleWireframe() { 276 | this.wireframe = !this.wireframe; 277 | } 278 | } -------------------------------------------------------------------------------- /demo/script.js: -------------------------------------------------------------------------------- 1 | class WaterRippleSimulation { 2 | constructor(canvas) { 3 | this.canvas = canvas; 4 | this.ctx = canvas.getContext('2d'); 5 | this.width = canvas.width; 6 | this.height = canvas.height; 7 | 8 | // 水面参数 9 | this.resolution = 4; // 网格分辨率 10 | this.cols = Math.floor(this.width / this.resolution); 11 | this.rows = Math.floor(this.height / this.resolution); 12 | 13 | // 水面高度数组 14 | this.currentHeight = []; 15 | this.previousHeight = []; 16 | this.velocity = []; 17 | 18 | // 物理参数 19 | this.damping = 0.95; 20 | this.waveStrength = 5; 21 | 22 | // 鼠标交互 23 | this.isMouseDown = false; 24 | this.lastMouseX = 0; 25 | this.lastMouseY = 0; 26 | 27 | // 渲染参数 28 | this.renderMode = '3d'; // '3d' 或 '2d' 29 | 30 | this.initArrays(); 31 | this.setupEventListeners(); 32 | this.animate(); 33 | } 34 | 35 | initArrays() { 36 | for (let i = 0; i < this.cols * this.rows; i++) { 37 | this.currentHeight[i] = 0; 38 | this.previousHeight[i] = 0; 39 | this.velocity[i] = 0; 40 | } 41 | } 42 | 43 | setupEventListeners() { 44 | // 鼠标事件 45 | this.canvas.addEventListener('mousedown', (e) => { 46 | this.isMouseDown = true; 47 | const rect = this.canvas.getBoundingClientRect(); 48 | this.lastMouseX = e.clientX - rect.left; 49 | this.lastMouseY = e.clientY - rect.top; 50 | this.createRipple(this.lastMouseX, this.lastMouseY); 51 | }); 52 | 53 | this.canvas.addEventListener('mousemove', (e) => { 54 | if (this.isMouseDown) { 55 | const rect = this.canvas.getBoundingClientRect(); 56 | const mouseX = e.clientX - rect.left; 57 | const mouseY = e.clientY - rect.top; 58 | this.createRipple(mouseX, mouseY); 59 | this.lastMouseX = mouseX; 60 | this.lastMouseY = mouseY; 61 | } 62 | }); 63 | 64 | this.canvas.addEventListener('mouseup', () => { 65 | this.isMouseDown = false; 66 | }); 67 | 68 | // 触摸事件(移动设备支持) 69 | this.canvas.addEventListener('touchstart', (e) => { 70 | e.preventDefault(); 71 | const rect = this.canvas.getBoundingClientRect(); 72 | const touch = e.touches[0]; 73 | const touchX = touch.clientX - rect.left; 74 | const touchY = touch.clientY - rect.top; 75 | this.createRipple(touchX, touchY); 76 | }); 77 | 78 | this.canvas.addEventListener('touchmove', (e) => { 79 | e.preventDefault(); 80 | const rect = this.canvas.getBoundingClientRect(); 81 | const touch = e.touches[0]; 82 | const touchX = touch.clientX - rect.left; 83 | const touchY = touch.clientY - rect.top; 84 | this.createRipple(touchX, touchY); 85 | }); 86 | 87 | // 控制面板事件 88 | const dampingSlider = document.getElementById('damping'); 89 | const dampingValue = document.getElementById('dampingValue'); 90 | dampingSlider.addEventListener('input', (e) => { 91 | this.damping = parseFloat(e.target.value); 92 | dampingValue.textContent = this.damping; 93 | }); 94 | 95 | const waveStrengthSlider = document.getElementById('waveStrength'); 96 | const waveStrengthValue = document.getElementById('waveStrengthValue'); 97 | waveStrengthSlider.addEventListener('input', (e) => { 98 | this.waveStrength = parseInt(e.target.value); 99 | waveStrengthValue.textContent = this.waveStrength; 100 | }); 101 | 102 | const resetBtn = document.getElementById('resetBtn'); 103 | resetBtn.addEventListener('click', () => { 104 | this.initArrays(); 105 | }); 106 | } 107 | 108 | createRipple(x, y) { 109 | const col = Math.floor(x / this.resolution); 110 | const row = Math.floor(y / this.resolution); 111 | 112 | if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) { 113 | const index = row * this.cols + col; 114 | this.currentHeight[index] += this.waveStrength; 115 | } 116 | } 117 | 118 | updatePhysics() { 119 | // 水波物理模拟 120 | for (let row = 1; row < this.rows - 1; row++) { 121 | for (let col = 1; col < this.cols - 1; col++) { 122 | const index = row * this.cols + col; 123 | 124 | // 计算周围邻居的平均高度 125 | const neighbors = [ 126 | this.previousHeight[(row - 1) * this.cols + col], // 上 127 | this.previousHeight[(row + 1) * this.cols + col], // 下 128 | this.previousHeight[row * this.cols + col - 1], // 左 129 | this.previousHeight[row * this.cols + col + 1] // 右 130 | ]; 131 | 132 | const avgNeighborHeight = neighbors.reduce((sum, h) => sum + h, 0) / 4; 133 | 134 | // 更新当前高度(波动方程) 135 | const newHeight = avgNeighborHeight * 2 - this.currentHeight[index]; 136 | this.currentHeight[index] = newHeight * this.damping; 137 | } 138 | } 139 | 140 | // 交换数组 141 | [this.previousHeight, this.currentHeight] = [this.currentHeight, this.previousHeight]; 142 | } 143 | 144 | render() { 145 | this.ctx.clearRect(0, 0, this.width, this.height); 146 | 147 | if (this.renderMode === '3d') { 148 | this.render3D(); 149 | } else { 150 | this.render2D(); 151 | } 152 | } 153 | 154 | render3D() { 155 | // 3D渲染模式 - 使用阴影和光照效果 156 | const imageData = this.ctx.createImageData(this.width, this.height); 157 | const data = imageData.data; 158 | 159 | for (let row = 0; row < this.rows; row++) { 160 | for (let col = 0; col < this.cols; col++) { 161 | const index = row * this.cols + col; 162 | const height = this.previousHeight[index]; 163 | 164 | // 计算法向量(用于光照) 165 | const dx = (col < this.cols - 1) ? 166 | this.previousHeight[index + 1] - height : 0; 167 | const dy = (row < this.rows - 1) ? 168 | this.previousHeight[index + this.cols] - height : 0; 169 | 170 | // 简单的光照计算 171 | const lightX = 0.5; 172 | const lightY = -0.5; 173 | const lightZ = 1; 174 | 175 | const normalX = -dx / 10; 176 | const normalY = -dy / 10; 177 | const normalZ = 1; 178 | 179 | const dotProduct = normalX * lightX + normalY * lightY + normalZ * lightZ; 180 | const brightness = Math.max(0, Math.min(1, dotProduct)); 181 | 182 | // 根据高度和光照设置颜色 183 | const baseColor = this.getWaterColor(height); 184 | const r = Math.floor(baseColor.r * brightness); 185 | const g = Math.floor(baseColor.g * brightness); 186 | const b = Math.floor(baseColor.b * brightness); 187 | 188 | // 填充像素 189 | for (let dy = 0; dy < this.resolution; dy++) { 190 | for (let dx = 0; dx < this.resolution; dx++) { 191 | const pixelX = col * this.resolution + dx; 192 | const pixelY = row * this.resolution + dy; 193 | const pixelIndex = (pixelY * this.width + pixelX) * 4; 194 | 195 | if (pixelIndex < data.length) { 196 | data[pixelIndex] = r; // R 197 | data[pixelIndex + 1] = g; // G 198 | data[pixelIndex + 2] = b; // B 199 | data[pixelIndex + 3] = 255; // A 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | this.ctx.putImageData(imageData, 0, 0); 207 | } 208 | 209 | render2D() { 210 | // 2D渲染模式 - 简单的圆形波纹 211 | this.ctx.fillStyle = 'rgba(30, 60, 114, 0.8)'; 212 | this.ctx.fillRect(0, 0, this.width, this.height); 213 | 214 | for (let row = 0; row < this.rows; row++) { 215 | for (let col = 0; col < this.cols; col++) { 216 | const index = row * this.cols + col; 217 | const height = Math.abs(this.previousHeight[index]); 218 | 219 | if (height > 0.1) { 220 | const x = col * this.resolution + this.resolution / 2; 221 | const y = row * this.resolution + this.resolution / 2; 222 | const radius = Math.min(height * 2, this.resolution); 223 | 224 | const alpha = Math.min(height / 10, 1); 225 | this.ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; 226 | this.ctx.beginPath(); 227 | this.ctx.arc(x, y, radius, 0, Math.PI * 2); 228 | this.ctx.fill(); 229 | } 230 | } 231 | } 232 | } 233 | 234 | getWaterColor(height) { 235 | // 根据高度返回水的颜色 236 | const absHeight = Math.abs(height); 237 | 238 | if (height > 0) { 239 | // 波峰 - 更亮的蓝色 240 | const intensity = Math.min(absHeight / 10, 1); 241 | return { 242 | r: 100 + intensity * 155, 243 | g: 150 + intensity * 105, 244 | b: 255 245 | }; 246 | } else { 247 | // 波谷 - 更深的蓝色 248 | const intensity = Math.min(absHeight / 10, 1); 249 | return { 250 | r: 30 + intensity * 70, 251 | g: 60 + intensity * 90, 252 | b: 114 + intensity * 141 253 | }; 254 | } 255 | } 256 | 257 | animate() { 258 | this.updatePhysics(); 259 | this.render(); 260 | requestAnimationFrame(() => this.animate()); 261 | } 262 | } 263 | 264 | // 初始化应用 265 | document.addEventListener('DOMContentLoaded', () => { 266 | const canvas = document.getElementById('waterCanvas'); 267 | 268 | // 响应式画布大小 269 | function resizeCanvas() { 270 | const container = canvas.parentElement; 271 | const maxWidth = Math.min(800, container.clientWidth - 40); 272 | const maxHeight = Math.min(600, window.innerHeight - 300); 273 | 274 | canvas.width = maxWidth; 275 | canvas.height = maxHeight; 276 | canvas.style.width = maxWidth + 'px'; 277 | canvas.style.height = maxHeight + 'px'; 278 | } 279 | 280 | resizeCanvas(); 281 | window.addEventListener('resize', resizeCanvas); 282 | 283 | // 创建水波纹模拟实例 284 | const simulation = new WaterRippleSimulation(canvas); 285 | 286 | // 添加键盘快捷键 287 | document.addEventListener('keydown', (e) => { 288 | switch(e.key) { 289 | case 'r': 290 | case 'R': 291 | simulation.initArrays(); 292 | break; 293 | case 'm': 294 | case 'M': 295 | simulation.renderMode = simulation.renderMode === '3d' ? '2d' : '3d'; 296 | break; 297 | } 298 | }); 299 | 300 | console.log('水波纹模拟器已启动!'); 301 | console.log('快捷键:'); 302 | console.log('- R: 重置水面'); 303 | console.log('- M: 切换渲染模式'); 304 | }); -------------------------------------------------------------------------------- /demo/gobang_snake.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 五子棋盘上的贪吃蛇 7 | 145 | 146 | 147 |

五子棋盘上的贪吃蛇

148 |
149 |
150 | 151 | 152 | 153 |
154 |
155 | 得分: 0 156 | 长度: 3 157 |
158 |
159 |
160 |
161 |
162 | 163 | 347 | 348 | 349 | -------------------------------------------------------------------------------- /demo/js/physics.js: -------------------------------------------------------------------------------- 1 | // Physics engine for the racing game 2 | 3 | class PhysicsWorld { 4 | constructor() { 5 | this.world = new CANNON.World(); 6 | this.world.gravity.set(0, -9.82, 0); 7 | this.world.broadphase = new CANNON.NaiveBroadphase(); 8 | this.world.solver.iterations = 10; 9 | 10 | // Ground material 11 | this.groundMaterial = new CANNON.Material('ground'); 12 | this.carMaterial = new CANNON.Material('car'); 13 | 14 | // Contact materials 15 | this.setupContactMaterials(); 16 | 17 | this.bodies = new Map(); 18 | this.meshes = new Map(); 19 | } 20 | 21 | setupContactMaterials() { 22 | // Car-ground contact 23 | const carGroundContact = new CANNON.ContactMaterial( 24 | this.carMaterial, 25 | this.groundMaterial, 26 | { 27 | friction: 0.4, 28 | restitution: 0.3, 29 | contactEquationStiffness: 1e8, 30 | contactEquationRelaxation: 3 31 | } 32 | ); 33 | 34 | this.world.addContactMaterial(carGroundContact); 35 | } 36 | 37 | addBody(id, body, mesh = null) { 38 | this.world.add(body); 39 | this.bodies.set(id, body); 40 | if (mesh) { 41 | this.meshes.set(id, mesh); 42 | } 43 | } 44 | 45 | removeBody(id) { 46 | const body = this.bodies.get(id); 47 | if (body) { 48 | this.world.remove(body); 49 | this.bodies.delete(id); 50 | this.meshes.delete(id); 51 | } 52 | } 53 | 54 | update(deltaTime) { 55 | this.world.step(deltaTime); 56 | 57 | // Sync physics bodies with Three.js meshes 58 | this.meshes.forEach((mesh, id) => { 59 | const body = this.bodies.get(id); 60 | if (body && mesh) { 61 | mesh.position.copy(body.position); 62 | mesh.quaternion.copy(body.quaternion); 63 | } 64 | }); 65 | } 66 | 67 | raycast(from, to, options = {}) { 68 | const result = new CANNON.RaycastResult(); 69 | this.world.raycastClosest(from, to, options, result); 70 | return result; 71 | } 72 | } 73 | 74 | class CarPhysics { 75 | constructor(world, position = new CANNON.Vec3(0, 2, 0)) { 76 | this.world = world; 77 | this.chassisBody = null; 78 | this.wheels = []; 79 | this.constraints = []; 80 | 81 | this.mass = 1500; 82 | this.wheelMass = 50; 83 | this.maxSteerValue = 0.5; 84 | this.maxForce = 1500; 85 | this.maxSpeed = 50; 86 | this.brakeForce = 1000; 87 | 88 | this.currentSpeed = 0; 89 | this.engineForce = 0; 90 | this.steeringValue = 0; 91 | this.brakeForce = 0; 92 | 93 | this.setupChassis(position); 94 | this.setupWheels(); 95 | } 96 | 97 | setupChassis(position) { 98 | // Create chassis shape 99 | const chassisShape = new CANNON.Box(new CANNON.Vec3(2, 0.5, 4)); 100 | this.chassisBody = new CANNON.Body({ mass: this.mass, material: this.world.carMaterial }); 101 | this.chassisBody.addShape(chassisShape); 102 | this.chassisBody.position.copy(position); 103 | 104 | // Add some angular damping to prevent flipping 105 | this.chassisBody.angularDamping = 0.4; 106 | this.chassisBody.linearDamping = 0.1; 107 | 108 | this.world.addBody('chassis', this.chassisBody); 109 | } 110 | 111 | setupWheels() { 112 | const wheelPositions = [ 113 | new CANNON.Vec3(-1.5, -0.5, 2.5), // Front left 114 | new CANNON.Vec3(1.5, -0.5, 2.5), // Front right 115 | new CANNON.Vec3(-1.5, -0.5, -2.5), // Rear left 116 | new CANNON.Vec3(1.5, -0.5, -2.5) // Rear right 117 | ]; 118 | 119 | wheelPositions.forEach((position, index) => { 120 | const wheelShape = new CANNON.Sphere(0.5); 121 | const wheelBody = new CANNON.Body({ mass: this.wheelMass }); 122 | wheelBody.addShape(wheelShape); 123 | wheelBody.position.copy(this.chassisBody.position.vadd(position)); 124 | 125 | // Create constraint to attach wheel to chassis 126 | const constraint = new CANNON.PointToPointConstraint( 127 | this.chassisBody, 128 | position, 129 | wheelBody, 130 | new CANNON.Vec3(0, 0, 0) 131 | ); 132 | 133 | this.world.world.addConstraint(constraint); 134 | this.wheels.push({ 135 | body: wheelBody, 136 | position: position, 137 | isFront: index < 2, 138 | constraint: constraint 139 | }); 140 | }); 141 | } 142 | 143 | update(input, deltaTime) { 144 | // Calculate current speed 145 | this.currentSpeed = this.chassisBody.velocity.length(); 146 | 147 | // Handle steering 148 | if (input.left) { 149 | this.steeringValue = Math.min(this.steeringValue + 0.02, this.maxSteerValue); 150 | } else if (input.right) { 151 | this.steeringValue = Math.max(this.steeringValue - 0.02, -this.maxSteerValue); 152 | } else { 153 | this.steeringValue *= 0.9; // Return to center 154 | } 155 | 156 | // Handle acceleration/braking 157 | if (input.forward) { 158 | this.engineForce = this.maxForce; 159 | this.brakeForce = 0; 160 | } else if (input.backward) { 161 | this.engineForce = -this.maxForce * 0.5; 162 | this.brakeForce = 0; 163 | } else { 164 | this.engineForce = 0; 165 | this.brakeForce = input.brake ? this.brakeForce : 0; 166 | } 167 | 168 | // Apply forces 169 | this.applyEngineForce(); 170 | this.applySteering(); 171 | this.applyBraking(); 172 | 173 | // Apply downforce based on speed 174 | this.applyDownforce(); 175 | } 176 | 177 | applyEngineForce() { 178 | const forwardVector = new CANNON.Vec3(0, 0, 1); 179 | this.chassisBody.quaternion.vmult(forwardVector, forwardVector); 180 | 181 | // Apply force to rear wheels 182 | const rearWheelForce = this.engineForce / 2; 183 | this.wheels.forEach((wheel, index) => { 184 | if (index >= 2) { // Rear wheels 185 | const force = forwardVector.scale(rearWheelForce); 186 | wheel.body.applyForce(force, wheel.body.position); 187 | } 188 | }); 189 | } 190 | 191 | applySteering() { 192 | // Apply steering to front wheels 193 | this.wheels.forEach((wheel, index) => { 194 | if (index < 2) { // Front wheels 195 | const steerAngle = this.steeringValue; 196 | // Create a rotation quaternion for steering 197 | const steerQuat = new CANNON.Quaternion(); 198 | steerQuat.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), steerAngle); 199 | 200 | // Apply the steering rotation 201 | wheel.body.quaternion = this.chassisBody.quaternion.mult(steerQuat); 202 | } 203 | }); 204 | } 205 | 206 | applyBraking() { 207 | if (this.brakeForce > 0) { 208 | // Apply braking to all wheels 209 | this.wheels.forEach(wheel => { 210 | const velocity = wheel.body.velocity; 211 | const brakeVector = velocity.scale(-this.brakeForce * 0.1); 212 | wheel.body.applyForce(brakeVector, wheel.body.position); 213 | }); 214 | } 215 | } 216 | 217 | applyDownforce() { 218 | // Apply downforce proportional to speed squared 219 | const downforce = this.currentSpeed * this.currentSpeed * 0.5; 220 | const downforceVector = new CANNON.Vec3(0, -downforce, 0); 221 | this.chassisBody.applyForce(downforceVector, this.chassisBody.position); 222 | } 223 | 224 | getSpeed() { 225 | return this.currentSpeed; 226 | } 227 | 228 | getSpeedKMH() { 229 | return this.currentSpeed * 3.6; 230 | } 231 | 232 | getPosition() { 233 | return this.chassisBody.position; 234 | } 235 | 236 | getRotation() { 237 | return this.chassisBody.quaternion; 238 | } 239 | 240 | reset(position = new CANNON.Vec3(0, 2, 0)) { 241 | this.chassisBody.position.copy(position); 242 | this.chassisBody.velocity.set(0, 0, 0); 243 | this.chassisBody.angularVelocity.set(0, 0, 0); 244 | this.chassisBody.quaternion.set(0, 0, 0, 1); 245 | 246 | this.wheels.forEach((wheel, index) => { 247 | const wheelPos = position.vadd(wheel.position); 248 | wheel.body.position.copy(wheelPos); 249 | wheel.body.velocity.set(0, 0, 0); 250 | wheel.body.angularVelocity.set(0, 0, 0); 251 | wheel.body.quaternion.set(0, 0, 0, 1); 252 | }); 253 | 254 | this.engineForce = 0; 255 | this.steeringValue = 0; 256 | this.brakeForce = 0; 257 | } 258 | } 259 | 260 | class TrackPhysics { 261 | constructor(world) { 262 | this.world = world; 263 | this.trackBodies = []; 264 | this.barrierBodies = []; 265 | } 266 | 267 | createTrackSegment(points, width = 10, height = 0.5) { 268 | const segments = []; 269 | 270 | for (let i = 0; i < points.length - 1; i++) { 271 | const start = points[i]; 272 | const end = points[i + 1]; 273 | const segment = this.createSegment(start, end, width, height); 274 | segments.push(segment); 275 | } 276 | 277 | return segments; 278 | } 279 | 280 | createSegment(start, end, width, height) { 281 | const direction = end.clone().sub(start); 282 | const length = direction.length(); 283 | const center = start.clone().add(end).multiply(0.5); 284 | 285 | // Create track surface 286 | const trackShape = new CANNON.Box(new CANNON.Vec3(width / 2, height / 2, length / 2)); 287 | const trackBody = new CANNON.Body({ mass: 0, material: this.world.groundMaterial }); 288 | trackBody.addShape(trackShape); 289 | trackBody.position.copy(center); 290 | 291 | // Align with direction 292 | const angle = Math.atan2(direction.x, direction.z); 293 | trackBody.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), angle); 294 | 295 | this.world.addBody(`track_${Math.random()}`, trackBody); 296 | this.trackBodies.push(trackBody); 297 | 298 | // Create barriers 299 | this.createBarriers(start, end, width, height); 300 | 301 | return trackBody; 302 | } 303 | 304 | createBarriers(start, end, width, height) { 305 | const direction = end.clone().sub(start); 306 | const length = direction.length(); 307 | const center = start.clone().add(end).multiply(0.5); 308 | const angle = Math.atan2(direction.x, direction.z); 309 | 310 | // Left barrier 311 | const leftBarrierPos = center.clone(); 312 | leftBarrierPos.x += Math.sin(angle) * (width / 2 + 1); 313 | leftBarrierPos.z += Math.cos(angle) * (width / 2 + 1); 314 | 315 | const leftBarrierShape = new CANNON.Box(new CANNON.Vec3(0.5, 2, length / 2)); 316 | const leftBarrierBody = new CANNON.Body({ mass: 0 }); 317 | leftBarrierBody.addShape(leftBarrierShape); 318 | leftBarrierBody.position.copy(leftBarrierPos); 319 | leftBarrierBody.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), angle); 320 | 321 | // Right barrier 322 | const rightBarrierPos = center.clone(); 323 | rightBarrierPos.x -= Math.sin(angle) * (width / 2 + 1); 324 | rightBarrierPos.z -= Math.cos(angle) * (width / 2 + 1); 325 | 326 | const rightBarrierShape = new CANNON.Box(new CANNON.Vec3(0.5, 2, length / 2)); 327 | const rightBarrierBody = new CANNON.Body({ mass: 0 }); 328 | rightBarrierBody.addShape(rightBarrierShape); 329 | rightBarrierBody.position.copy(rightBarrierPos); 330 | rightBarrierBody.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), angle); 331 | 332 | this.world.addBody(`barrier_left_${Math.random()}`, leftBarrierBody); 333 | this.world.addBody(`barrier_right_${Math.random()}`, rightBarrierBody); 334 | this.barrierBodies.push(leftBarrierBody, rightBarrierBody); 335 | } 336 | 337 | clear() { 338 | this.trackBodies.forEach(body => { 339 | this.world.removeBody(`track_${Math.random()}`); 340 | }); 341 | this.barrierBodies.forEach(body => { 342 | this.world.removeBody(`barrier_${Math.random()}`); 343 | }); 344 | 345 | this.trackBodies = []; 346 | this.barrierBodies = []; 347 | } 348 | } 349 | 350 | // Export for use in other modules 351 | if (typeof module !== 'undefined' && module.exports) { 352 | module.exports = { 353 | PhysicsWorld, 354 | CarPhysics, 355 | TrackPhysics 356 | }; 357 | } -------------------------------------------------------------------------------- /demo/game.js: -------------------------------------------------------------------------------- 1 | // 3D赛车游戏主逻辑 2 | let scene, camera, renderer; 3 | let car, track; 4 | let keys = {}; 5 | let carSpeed = 0; 6 | let carRotation = 0; 7 | let gameState = 'menu'; // menu, playing, finished 8 | let startTime, currentTime; 9 | let laps = 0; 10 | let checkpoints = []; 11 | let lastCheckpoint = -1; 12 | 13 | // 游戏配置 14 | const config = { 15 | maxSpeed: 0.5, 16 | acceleration: 0.008, 17 | deceleration: 0.005, 18 | turnSpeed: 0.03, 19 | friction: 0.98 20 | }; 21 | 22 | function init() { 23 | // 创建场景 24 | scene = new THREE.Scene(); 25 | scene.fog = new THREE.Fog(0x87CEEB, 100, 500); 26 | 27 | // 创建相机 28 | camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 29 | camera.position.set(0, 5, 10); 30 | 31 | // 创建渲染器 32 | renderer = new THREE.WebGLRenderer({ antialias: true }); 33 | renderer.setSize(window.innerWidth, window.innerHeight); 34 | renderer.shadowMap.enabled = true; 35 | renderer.shadowMap.type = THREE.PCFSoftShadowMap; 36 | renderer.setClearColor(0x87CEEB); 37 | document.body.appendChild(renderer.domElement); 38 | renderer.domElement.id = 'gameCanvas'; 39 | 40 | // 创建光照 41 | setupLighting(); 42 | 43 | // 创建赛道 44 | createTrack(); 45 | 46 | // 创建赛车 47 | createCar(); 48 | 49 | // 创建环境 50 | createEnvironment(); 51 | 52 | // 事件监听 53 | window.addEventListener('resize', onWindowResize); 54 | window.addEventListener('keydown', (e) => keys[e.key] = true); 55 | window.addEventListener('keyup', (e) => keys[e.key] = false); 56 | } 57 | 58 | function setupLighting() { 59 | // 环境光 60 | const ambientLight = new THREE.AmbientLight(0x404040, 0.6); 61 | scene.add(ambientLight); 62 | 63 | // 方向光(太阳光) 64 | const directionalLight = new THREE.DirectionalLight(0xffffff, 1); 65 | directionalLight.position.set(50, 100, 50); 66 | directionalLight.castShadow = true; 67 | directionalLight.shadow.mapSize.width = 2048; 68 | directionalLight.shadow.mapSize.height = 2048; 69 | directionalLight.shadow.camera.near = 0.5; 70 | directionalLight.shadow.camera.far = 500; 71 | directionalLight.shadow.camera.left = -100; 72 | directionalLight.shadow.camera.right = 100; 73 | directionalLight.shadow.camera.top = 100; 74 | directionalLight.shadow.camera.bottom = -100; 75 | scene.add(directionalLight); 76 | } 77 | 78 | function createTrack() { 79 | // 创建赛道几何体 80 | const trackGeometry = new THREE.RingGeometry(20, 40, 32); 81 | const trackMaterial = new THREE.MeshLambertMaterial({ 82 | color: 0x333333, 83 | side: THREE.DoubleSide 84 | }); 85 | 86 | track = new THREE.Mesh(trackGeometry, trackMaterial); 87 | track.rotation.x = -Math.PI / 2; 88 | track.receiveShadow = true; 89 | scene.add(track); 90 | 91 | // 创建赛道边界 92 | createTrackBorders(); 93 | 94 | // 创建检查点 95 | createCheckpoints(); 96 | 97 | // 创建地面 98 | const groundGeometry = new THREE.PlaneGeometry(200, 200); 99 | const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x90EE90 }); 100 | const ground = new THREE.Mesh(groundGeometry, groundMaterial); 101 | ground.rotation.x = -Math.PI / 2; 102 | ground.position.y = -0.1; 103 | ground.receiveShadow = true; 104 | scene.add(ground); 105 | } 106 | 107 | function createTrackBorders() { 108 | // 外边界 109 | const outerBorderGeometry = new THREE.TorusGeometry(40, 0.5, 8, 100); 110 | const borderMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 }); 111 | const outerBorder = new THREE.Mesh(outerBorderGeometry, borderMaterial); 112 | outerBorder.rotation.x = -Math.PI / 2; 113 | outerBorder.position.y = 0.5; 114 | scene.add(outerBorder); 115 | 116 | // 内边界 117 | const innerBorderGeometry = new THREE.TorusGeometry(20, 0.5, 8, 100); 118 | const innerBorder = new THREE.Mesh(innerBorderGeometry, borderMaterial); 119 | innerBorder.rotation.x = -Math.PI / 2; 120 | innerBorder.position.y = 0.5; 121 | scene.add(innerBorder); 122 | } 123 | 124 | function createCheckpoints() { 125 | const checkpointCount = 4; 126 | for (let i = 0; i < checkpointCount; i++) { 127 | const angle = (i / checkpointCount) * Math.PI * 2; 128 | const radius = 30; 129 | 130 | const checkpointGeometry = new THREE.BoxGeometry(2, 5, 0.2); 131 | const checkpointMaterial = new THREE.MeshBasicMaterial({ 132 | color: 0xffff00, 133 | transparent: true, 134 | opacity: 0.3 135 | }); 136 | 137 | const checkpoint = new THREE.Mesh(checkpointGeometry, checkpointMaterial); 138 | checkpoint.position.x = Math.cos(angle) * radius; 139 | checkpoint.position.z = Math.sin(angle) * radius; 140 | checkpoint.position.y = 2.5; 141 | checkpoint.userData = { id: i, passed: false }; 142 | 143 | scene.add(checkpoint); 144 | checkpoints.push(checkpoint); 145 | } 146 | } 147 | 148 | function createCar() { 149 | // 车身 150 | const carBodyGeometry = new THREE.BoxGeometry(2, 0.8, 4); 151 | const carBodyMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 }); 152 | const carBody = new THREE.Mesh(carBodyGeometry, carBodyMaterial); 153 | carBody.position.y = 0.5; 154 | carBody.castShadow = true; 155 | 156 | // 车窗 157 | const windowGeometry = new THREE.BoxGeometry(1.6, 0.4, 1.5); 158 | const windowMaterial = new THREE.MeshLambertMaterial({ color: 0x4444ff, transparent: true, opacity: 0.7 }); 159 | const carWindow = new THREE.Mesh(windowGeometry, windowMaterial); 160 | carWindow.position.set(0, 0.9, 0.5); 161 | 162 | // 车轮 163 | const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16); 164 | const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x222222 }); 165 | 166 | const wheels = []; 167 | const wheelPositions = [ 168 | { x: -0.8, y: 0.2, z: 1.2 }, 169 | { x: 0.8, y: 0.2, z: 1.2 }, 170 | { x: -0.8, y: 0.2, z: -1.2 }, 171 | { x: 0.8, y: 0.2, z: -1.2 } 172 | ]; 173 | 174 | wheelPositions.forEach(pos => { 175 | const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); 176 | wheel.position.set(pos.x, pos.y, pos.z); 177 | wheel.rotation.z = Math.PI / 2; 178 | wheel.castShadow = true; 179 | wheels.push(wheel); 180 | }); 181 | 182 | // 创建赛车组 183 | car = new THREE.Group(); 184 | car.add(carBody); 185 | car.add(carWindow); 186 | wheels.forEach(wheel => car.add(wheel)); 187 | 188 | // 设置初始位置 189 | car.position.set(30, 0, 0); 190 | car.userData.wheels = wheels; 191 | 192 | scene.add(car); 193 | } 194 | 195 | function createEnvironment() { 196 | // 创建一些装饰性的树木 197 | for (let i = 0; i < 20; i++) { 198 | const tree = createTree(); 199 | const angle = Math.random() * Math.PI * 2; 200 | const distance = 50 + Math.random() * 30; 201 | tree.position.x = Math.cos(angle) * distance; 202 | tree.position.z = Math.sin(angle) * distance; 203 | scene.add(tree); 204 | } 205 | 206 | // 创建建筑物 207 | for (let i = 0; i < 10; i++) { 208 | const building = createBuilding(); 209 | const angle = Math.random() * Math.PI * 2; 210 | const distance = 60 + Math.random() * 40; 211 | building.position.x = Math.cos(angle) * distance; 212 | building.position.z = Math.sin(angle) * distance; 213 | scene.add(building); 214 | } 215 | } 216 | 217 | function createTree() { 218 | const tree = new THREE.Group(); 219 | 220 | // 树干 221 | const trunkGeometry = new THREE.CylinderGeometry(0.5, 0.8, 4); 222 | const trunkMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); 223 | const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); 224 | trunk.position.y = 2; 225 | trunk.castShadow = true; 226 | 227 | // 树冠 228 | const foliageGeometry = new THREE.SphereGeometry(3, 8, 6); 229 | const foliageMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 }); 230 | const foliage = new THREE.Mesh(foliageGeometry, foliageMaterial); 231 | foliage.position.y = 5; 232 | foliage.castShadow = true; 233 | 234 | tree.add(trunk); 235 | tree.add(foliage); 236 | 237 | return tree; 238 | } 239 | 240 | function createBuilding() { 241 | const height = 5 + Math.random() * 15; 242 | const buildingGeometry = new THREE.BoxGeometry(5, height, 5); 243 | const buildingMaterial = new THREE.MeshLambertMaterial({ 244 | color: new THREE.Color(0.5 + Math.random() * 0.5, 0.5 + Math.random() * 0.5, 0.5 + Math.random() * 0.5) 245 | }); 246 | const building = new THREE.Mesh(buildingGeometry, buildingMaterial); 247 | building.position.y = height / 2; 248 | building.castShadow = true; 249 | building.receiveShadow = true; 250 | 251 | return building; 252 | } 253 | 254 | function updateCar() { 255 | if (gameState !== 'playing') return; 256 | 257 | // 处理输入 258 | if (keys['ArrowUp']) { 259 | carSpeed = Math.min(carSpeed + config.acceleration, config.maxSpeed); 260 | } else if (keys['ArrowDown']) { 261 | carSpeed = Math.max(carSpeed - config.acceleration * 2, -config.maxSpeed / 2); 262 | } else { 263 | carSpeed *= config.friction; 264 | } 265 | 266 | // 转向(只有在移动时才能转向) 267 | if (Math.abs(carSpeed) > 0.01) { 268 | if (keys['ArrowLeft']) { 269 | carRotation += config.turnSpeed * (carSpeed / config.maxSpeed); 270 | } 271 | if (keys['ArrowRight']) { 272 | carRotation -= config.turnSpeed * (carSpeed / config.maxSpeed); 273 | } 274 | } 275 | 276 | // 手刹 277 | if (keys[' ']) { 278 | carSpeed *= 0.95; 279 | } 280 | 281 | // 重置位置 282 | if (keys['r'] || keys['R']) { 283 | car.position.set(30, 0, 0); 284 | car.rotation.y = 0; 285 | carSpeed = 0; 286 | carRotation = 0; 287 | } 288 | 289 | // 应用移动 290 | car.rotation.y = carRotation; 291 | car.position.x += Math.sin(carRotation) * carSpeed; 292 | car.position.z += Math.cos(carRotation) * carSpeed; 293 | 294 | // 边界检测 295 | const distance = Math.sqrt(car.position.x * car.position.x + car.position.z * car.position.z); 296 | if (distance < 20 || distance > 40) { 297 | carSpeed *= 0.5; // 撞墙减速 298 | // 推回赛道内 299 | const pushDirection = distance < 20 ? 1 : -1; 300 | car.position.x = (car.position.x / distance) * (20 + 1) * pushDirection; 301 | car.position.z = (car.position.z / distance) * (20 + 1) * pushDirection; 302 | } 303 | 304 | // 更新车轮旋转 305 | if (car.userData.wheels) { 306 | car.userData.wheels.forEach(wheel => { 307 | wheel.rotation.x += carSpeed * 2; 308 | }); 309 | } 310 | 311 | // 检查检查点 312 | checkCheckpoints(); 313 | } 314 | 315 | function checkCheckpoints() { 316 | checkpoints.forEach(checkpoint => { 317 | const distance = car.position.distanceTo(checkpoint.position); 318 | if (distance < 10 && !checkpoint.userData.passed) { 319 | checkpoint.userData.passed = true; 320 | 321 | // 检查是否按顺序通过所有检查点 322 | const expectedCheckpoint = (lastCheckpoint + 1) % checkpoints.length; 323 | if (checkpoint.userData.id === expectedCheckpoint) { 324 | lastCheckpoint = expectedCheckpoint; 325 | 326 | // 如果完成一圈 327 | if (expectedCheckpoint === 0 && lastCheckpoint > 0) { 328 | laps++; 329 | if (laps >= 3) { 330 | gameState = 'finished'; 331 | alert(`恭喜完成!用时: ${Math.floor((Date.now() - startTime) / 1000)}秒`); 332 | } 333 | } 334 | } 335 | } 336 | }); 337 | } 338 | 339 | function updateCamera() { 340 | // 第三人称相机跟随 341 | const idealOffset = new THREE.Vector3( 342 | Math.sin(carRotation) * -10, 343 | 5, 344 | Math.cos(carRotation) * -10 345 | ); 346 | 347 | camera.position.lerp( 348 | car.position.clone().add(idealOffset), 349 | 0.1 350 | ); 351 | camera.lookAt(car.position); 352 | } 353 | 354 | function updateUI() { 355 | if (gameState === 'playing') { 356 | document.getElementById('speed').textContent = Math.floor(Math.abs(carSpeed) * 200); 357 | document.getElementById('laps').textContent = laps; 358 | document.getElementById('time').textContent = Math.floor((Date.now() - startTime) / 1000); 359 | } 360 | } 361 | 362 | function animate() { 363 | requestAnimationFrame(animate); 364 | 365 | updateCar(); 366 | updateCamera(); 367 | updateUI(); 368 | 369 | renderer.render(scene, camera); 370 | } 371 | 372 | function onWindowResize() { 373 | camera.aspect = window.innerWidth / window.innerHeight; 374 | camera.updateProjectionMatrix(); 375 | renderer.setSize(window.innerWidth, window.innerHeight); 376 | } 377 | 378 | function startGame() { 379 | // 隐藏开始画面,显示游戏UI 380 | document.getElementById('startScreen').style.display = 'none'; 381 | document.getElementById('ui').style.display = 'block'; 382 | document.getElementById('controls').style.display = 'block'; 383 | 384 | // 重置游戏状态 385 | gameState = 'playing'; 386 | startTime = Date.now(); 387 | laps = 0; 388 | lastCheckpoint = -1; 389 | carSpeed = 0; 390 | carRotation = 0; 391 | 392 | // 重置赛车位置 393 | car.position.set(30, 0, 0); 394 | car.rotation.y = 0; 395 | 396 | // 重置检查点 397 | checkpoints.forEach(checkpoint => { 398 | checkpoint.userData.passed = false; 399 | }); 400 | } 401 | 402 | // 启动游戏 403 | init(); 404 | animate(); -------------------------------------------------------------------------------- /demo/js/sound.js: -------------------------------------------------------------------------------- 1 | // Sound effects and background music management 2 | 3 | class SoundManager { 4 | constructor() { 5 | this.audioContext = null; 6 | this.sounds = new Map(); 7 | this.music = null; 8 | this.masterVolume = 0.7; 9 | this.sfxVolume = 0.8; 10 | this.musicVolume = 0.5; 11 | this.enabled = true; 12 | 13 | this.initializeAudio(); 14 | } 15 | 16 | async initializeAudio() { 17 | try { 18 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); 19 | 20 | // Resume audio context on user interaction 21 | document.addEventListener('click', () => { 22 | if (this.audioContext.state === 'suspended') { 23 | this.audioContext.resume(); 24 | } 25 | }, { once: true }); 26 | 27 | console.log('Audio context initialized'); 28 | } catch (error) { 29 | console.warn('Web Audio API not supported:', error); 30 | this.fallbackToHTMLAudio(); 31 | } 32 | } 33 | 34 | fallbackToHTMLAudio() { 35 | // Fallback to HTML5 Audio for older browsers 36 | this.useHTMLAudio = true; 37 | console.log('Using HTML5 Audio fallback'); 38 | } 39 | 40 | async loadSound(name, url) { 41 | try { 42 | if (this.useHTMLAudio) { 43 | return this.loadHTMLAudio(name, url); 44 | } else { 45 | return this.loadWebAudio(name, url); 46 | } 47 | } catch (error) { 48 | console.error(`Failed to load sound ${name}:`, error); 49 | return null; 50 | } 51 | } 52 | 53 | async loadWebAudio(name, url) { 54 | const response = await fetch(url); 55 | const arrayBuffer = await response.arrayBuffer(); 56 | const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); 57 | 58 | this.sounds.set(name, { 59 | buffer: audioBuffer, 60 | type: 'webaudio' 61 | }); 62 | 63 | return audioBuffer; 64 | } 65 | 66 | async loadHTMLAudio(name, url) { 67 | const audio = new Audio(url); 68 | audio.preload = 'auto'; 69 | 70 | return new Promise((resolve, reject) => { 71 | audio.addEventListener('canplaythrough', () => { 72 | this.sounds.set(name, { 73 | audio: audio, 74 | type: 'htmlaudio' 75 | }); 76 | resolve(audio); 77 | }); 78 | 79 | audio.addEventListener('error', reject); 80 | }); 81 | } 82 | 83 | playSound(name, volume = 1.0, pitch = 1.0, loop = false) { 84 | if (!this.enabled || !this.sounds.has(name)) return null; 85 | 86 | const sound = this.sounds.get(name); 87 | const finalVolume = volume * this.sfxVolume * this.masterVolume; 88 | 89 | if (sound.type === 'webaudio') { 90 | return this.playWebAudio(sound.buffer, finalVolume, pitch, loop); 91 | } else { 92 | return this.playHTMLAudio(sound.audio, finalVolume, pitch, loop); 93 | } 94 | } 95 | 96 | playWebAudio(buffer, volume, pitch, loop) { 97 | if (!this.audioContext) return null; 98 | 99 | const source = this.audioContext.createBufferSource(); 100 | const gainNode = this.audioContext.createGain(); 101 | 102 | source.buffer = buffer; 103 | source.playbackRate.value = pitch; 104 | source.loop = loop; 105 | 106 | gainNode.gain.value = volume; 107 | 108 | source.connect(gainNode); 109 | gainNode.connect(this.audioContext.destination); 110 | 111 | source.start(); 112 | 113 | return { 114 | stop: () => source.stop(), 115 | setVolume: (vol) => gainNode.gain.value = vol * this.sfxVolume * this.masterVolume, 116 | setPitch: (pit) => source.playbackRate.value = pit 117 | }; 118 | } 119 | 120 | playHTMLAudio(audio, volume, pitch, loop) { 121 | const clonedAudio = audio.cloneNode(); 122 | clonedAudio.volume = volume; 123 | clonedAudio.playbackRate = pitch; 124 | clonedAudio.loop = loop; 125 | clonedAudio.play(); 126 | 127 | return { 128 | stop: () => clonedAudio.pause(), 129 | setVolume: (vol) => clonedAudio.volume = vol * this.sfxVolume * this.masterVolume, 130 | setPitch: (pit) => clonedAudio.playbackRate = pit 131 | }; 132 | } 133 | 134 | playMusic(name, volume = 1.0, loop = true) { 135 | if (!this.enabled) return; 136 | 137 | // Stop current music 138 | this.stopMusic(); 139 | 140 | // Play new music 141 | this.music = this.playSound(name, volume, 1.0, loop); 142 | } 143 | 144 | stopMusic() { 145 | if (this.music) { 146 | this.music.stop(); 147 | this.music = null; 148 | } 149 | } 150 | 151 | setMasterVolume(volume) { 152 | this.masterVolume = MathUtils.clamp(volume, 0, 1); 153 | } 154 | 155 | setSFXVolume(volume) { 156 | this.sfxVolume = MathUtils.clamp(volume, 0, 1); 157 | } 158 | 159 | setMusicVolume(volume) { 160 | this.musicVolume = MathUtils.clamp(volume, 0, 1); 161 | if (this.music) { 162 | this.music.setVolume(this.musicVolume); 163 | } 164 | } 165 | 166 | toggleSound() { 167 | this.enabled = !this.enabled; 168 | if (!this.enabled) { 169 | this.stopMusic(); 170 | } 171 | } 172 | 173 | // Generate procedural sounds 174 | generateEngineSound(rpm, load = 0.5) { 175 | if (!this.audioContext || !this.enabled) return; 176 | 177 | // Create engine sound using oscillators 178 | const oscillator1 = this.audioContext.createOscillator(); 179 | const oscillator2 = this.audioContext.createOscillator(); 180 | const gainNode = this.audioContext.createGain(); 181 | const filter = this.audioContext.createBiquadFilter(); 182 | 183 | // Calculate frequencies based on RPM 184 | const baseFreq = MathUtils.map(rpm, 0, 8000, 50, 400); 185 | const harmonicFreq = baseFreq * 2; 186 | 187 | oscillator1.frequency.value = baseFreq; 188 | oscillator2.frequency.value = harmonicFreq; 189 | 190 | // Set waveform types 191 | oscillator1.type = 'sawtooth'; 192 | oscillator2.type = 'square'; 193 | 194 | // Set filter 195 | filter.type = 'lowpass'; 196 | filter.frequency.value = MathUtils.map(rpm, 0, 8000, 200, 2000); 197 | filter.Q.value = 1; 198 | 199 | // Set volume based on load 200 | const volume = MathUtils.map(load, 0, 1, 0.1, 0.3) * this.sfxVolume * this.masterVolume; 201 | gainNode.gain.value = volume; 202 | 203 | // Connect nodes 204 | oscillator1.connect(filter); 205 | oscillator2.connect(filter); 206 | filter.connect(gainNode); 207 | gainNode.connect(this.audioContext.destination); 208 | 209 | // Start and schedule stop 210 | oscillator1.start(); 211 | oscillator2.start(); 212 | 213 | // Stop after a short duration 214 | const duration = 0.1; 215 | oscillator1.stop(this.audioContext.currentTime + duration); 216 | oscillator2.stop(this.audioContext.currentTime + duration); 217 | 218 | return { 219 | oscillators: [oscillator1, oscillator2], 220 | gain: gainNode, 221 | filter: filter 222 | }; 223 | } 224 | 225 | generateTireScreech(speed, intensity = 1.0) { 226 | if (!this.audioContext || !this.enabled) return; 227 | 228 | const oscillator = this.audioContext.createOscillator(); 229 | const gainNode = this.audioContext.createGain(); 230 | const filter = this.audioContext.createBiquadFilter(); 231 | 232 | // Create screeching sound 233 | const freq = MathUtils.map(speed, 0, 200, 800, 2000); 234 | oscillator.frequency.value = freq; 235 | oscillator.type = 'sawtooth'; 236 | 237 | // Set filter for screeching effect 238 | filter.type = 'bandpass'; 239 | filter.frequency.value = freq; 240 | filter.Q.value = 10; 241 | 242 | // Set volume 243 | const volume = intensity * 0.2 * this.sfxVolume * this.masterVolume; 244 | gainNode.gain.value = volume; 245 | gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.5); 246 | 247 | // Connect nodes 248 | oscillator.connect(filter); 249 | filter.connect(gainNode); 250 | gainNode.connect(this.audioContext.destination); 251 | 252 | // Start and stop 253 | oscillator.start(); 254 | oscillator.stop(this.audioContext.currentTime + 0.5); 255 | 256 | return { oscillator, gain: gainNode, filter }; 257 | } 258 | 259 | generateCollisionSound(intensity = 1.0) { 260 | if (!this.audioContext || !this.enabled) return; 261 | 262 | // Create collision sound using noise 263 | const bufferSize = this.audioContext.sampleRate * 0.2; 264 | const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); 265 | const data = buffer.getChannelData(0); 266 | 267 | // Generate white noise 268 | for (let i = 0; i < bufferSize; i++) { 269 | data[i] = (Math.random() - 0.5) * intensity; 270 | } 271 | 272 | const source = this.audioContext.createBufferSource(); 273 | const gainNode = this.audioContext.createGain(); 274 | const filter = this.audioContext.createBiquadFilter(); 275 | 276 | source.buffer = buffer; 277 | 278 | // Set filter for impact sound 279 | filter.type = 'lowpass'; 280 | filter.frequency.value = 500; 281 | 282 | // Set volume envelope 283 | const volume = intensity * 0.3 * this.sfxVolume * this.masterVolume; 284 | gainNode.gain.value = volume; 285 | gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.2); 286 | 287 | // Connect nodes 288 | source.connect(filter); 289 | filter.connect(gainNode); 290 | gainNode.connect(this.audioContext.destination); 291 | 292 | // Start and stop 293 | source.start(); 294 | source.stop(this.audioContext.currentTime + 0.2); 295 | 296 | return { source, gain: gainNode, filter }; 297 | } 298 | 299 | generateBoostSound() { 300 | if (!this.audioContext || !this.enabled) return; 301 | 302 | const oscillator = this.audioContext.createOscillator(); 303 | const gainNode = this.audioContext.createGain(); 304 | const filter = this.audioContext.createBiquadFilter(); 305 | 306 | // Create rising pitch for boost 307 | oscillator.frequency.setValueAtTime(200, this.audioContext.currentTime); 308 | oscillator.frequency.exponentialRampToValueAtTime(800, this.audioContext.currentTime + 0.3); 309 | oscillator.type = 'sawtooth'; 310 | 311 | // Set filter 312 | filter.type = 'lowpass'; 313 | filter.frequency.value = 1000; 314 | 315 | // Set volume 316 | gainNode.gain.value = 0.2 * this.sfxVolume * this.masterVolume; 317 | 318 | // Connect nodes 319 | oscillator.connect(filter); 320 | filter.connect(gainNode); 321 | gainNode.connect(this.audioContext.destination); 322 | 323 | // Start and stop 324 | oscillator.start(); 325 | oscillator.stop(this.audioContext.currentTime + 0.3); 326 | 327 | return { oscillator, gain: gainNode, filter }; 328 | } 329 | } 330 | 331 | // Car-specific sound effects 332 | class CarSoundEffects { 333 | constructor(soundManager, car) { 334 | this.soundManager = soundManager; 335 | this.car = car; 336 | 337 | this.engineSound = null; 338 | this.tireSound = null; 339 | this.lastEngineSound = 0; 340 | this.lastTireSound = 0; 341 | 342 | this.engineSoundInterval = 0.05; 343 | this.tireSoundInterval = 0.1; 344 | } 345 | 346 | update(deltaTime) { 347 | if (!this.soundManager.enabled) return; 348 | 349 | const currentTime = Date.now() / 1000; 350 | const rpm = this.car.getRPM(); 351 | const speed = this.car.getSpeed(); 352 | const throttle = this.car.throttle; 353 | const steering = Math.abs(this.car.steering); 354 | 355 | // Update engine sound 356 | if (currentTime - this.lastEngineSound > this.engineSoundInterval) { 357 | this.updateEngineSound(rpm, throttle); 358 | this.lastEngineSound = currentTime; 359 | } 360 | 361 | // Update tire screech sound 362 | if (currentTime - this.lastTireSound > this.tireSoundInterval) { 363 | this.updateTireSound(speed, steering); 364 | this.lastTireSound = currentTime; 365 | } 366 | } 367 | 368 | updateEngineSound(rpm, throttle) { 369 | const load = throttle; 370 | this.soundManager.generateEngineSound(rpm, load); 371 | } 372 | 373 | updateTireSound(speed, steering) { 374 | if (steering > 0.7 && speed > 30) { 375 | const intensity = Math.min(steering * speed / 100, 1); 376 | this.soundManager.generateTireScreech(speed, intensity); 377 | } 378 | } 379 | 380 | playCollisionSound(intensity = 1.0) { 381 | this.soundManager.generateCollisionSound(intensity); 382 | } 383 | 384 | playBoostSound() { 385 | this.soundManager.generateBoostSound(); 386 | } 387 | 388 | playBrakeSound() { 389 | if (this.car.brake > 0.5) { 390 | this.soundManager.generateTireScreech(this.car.getSpeed(), 0.5); 391 | } 392 | } 393 | } 394 | 395 | // Racing game sound effects 396 | class RacingGameSounds { 397 | constructor(soundManager) { 398 | this.soundManager = soundManager; 399 | this.loaded = false; 400 | this.sounds = { 401 | engine: null, 402 | collision: null, 403 | boost: null, 404 | countdown: null, 405 | finish: null, 406 | lap: null, 407 | menu: null 408 | }; 409 | } 410 | 411 | async loadSounds() { 412 | try { 413 | // Load sound effects (these would be actual audio files) 414 | // For now, we'll use procedural generation 415 | this.loaded = true; 416 | console.log('Racing game sounds loaded'); 417 | } catch (error) { 418 | console.error('Failed to load racing sounds:', error); 419 | } 420 | } 421 | 422 | playCountdownSound(count) { 423 | if (!this.loaded) return; 424 | 425 | // Create countdown beep 426 | const frequency = count === 0 ? 800 : 400; 427 | this.soundManager.playTone(frequency, 0.2, 0.5); 428 | } 429 | 430 | playFinishSound() { 431 | if (!this.loaded) return; 432 | 433 | // Create victory sound 434 | this.soundManager.playMelody([523, 659, 784, 1047], [0.2, 0.2, 0.2, 0.4], 0.6); 435 | } 436 | 437 | playLapSound() { 438 | if (!this.loaded) return; 439 | 440 | // Create lap completion sound 441 | this.soundManager.playTone(600, 0.1, 0.3); 442 | setTimeout(() => this.soundManager.playTone(800, 0.1, 0.3), 100); 443 | } 444 | } 445 | 446 | // Extend SoundManager with additional methods 447 | SoundManager.prototype.playTone = function(frequency, duration, volume = 1.0) { 448 | if (!this.audioContext || !this.enabled) return; 449 | 450 | const oscillator = this.audioContext.createOscillator(); 451 | const gainNode = this.audioContext.createGain(); 452 | 453 | oscillator.frequency.value = frequency; 454 | oscillator.type = 'sine'; 455 | 456 | gainNode.gain.value = volume * this.sfxVolume * this.masterVolume; 457 | gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); 458 | 459 | oscillator.connect(gainNode); 460 | gainNode.connect(this.audioContext.destination); 461 | 462 | oscillator.start(); 463 | oscillator.stop(this.audioContext.currentTime + duration); 464 | 465 | return { oscillator, gain: gainNode }; 466 | }; 467 | 468 | SoundManager.prototype.playMelody = function(frequencies, durations, volume = 1.0) { 469 | if (!this.audioContext || !this.enabled) return; 470 | 471 | let currentTime = this.audioContext.currentTime; 472 | 473 | frequencies.forEach((freq, index) => { 474 | const oscillator = this.audioContext.createOscillator(); 475 | const gainNode = this.audioContext.createGain(); 476 | 477 | oscillator.frequency.value = freq; 478 | oscillator.type = 'sine'; 479 | 480 | gainNode.gain.value = 0; 481 | gainNode.gain.setValueAtTime(volume * this.sfxVolume * this.masterVolume, currentTime); 482 | gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + durations[index]); 483 | 484 | oscillator.connect(gainNode); 485 | gainNode.connect(this.audioContext.destination); 486 | 487 | oscillator.start(currentTime); 488 | oscillator.stop(currentTime + durations[index]); 489 | 490 | currentTime += durations[index]; 491 | }); 492 | }; 493 | 494 | // Export for use in other modules 495 | if (typeof module !== 'undefined' && module.exports) { 496 | module.exports = { 497 | SoundManager, 498 | CarSoundEffects, 499 | RacingGameSounds 500 | }; 501 | } -------------------------------------------------------------------------------- /articles/v1文章.md: -------------------------------------------------------------------------------- 1 | Claude Code 没有秘密!价值 3000 万美金的 400 行代码 2 | 首先,这又是一个标题党。 3 | 4 | 接着,让我们回到正题。 5 | 6 | 为什么Claude Code 比Cursor 调用同一个模型API但效果牛 100 倍? 7 | Claude Code 到底有什么工程设计上的秘密? 8 | 答案是:Claude Code 没有秘密。 9 | 10 | 11 | 如果一定要说有,就是model as agent。 12 | 什么意思呢?也就是模型才是agent,代码只是agent模型的道具。 13 | 14 | 模型as agent 思想是关键。 15 | 16 | 模型是80%,代码是20%。 17 | 18 | 19 | 20 | 市面上哪些已有模型勉强是agent 模型? 21 | claude sonnet, kimi k2 0905, glm4.5, qwen-coder,gpt5-codex,cwm code world model etc. 22 | 之前的模型都是QA 模型,训练目标是回答用户输入问题的答案,而不是独立长时间连续超多轮工具调用以完成用户要求的工作。 23 | 24 | 25 | 26 | 我们做了个 0 - 1手搓mini Kode 的仓库,每天更新一部分代码,让你这个国庆假期 7 天学个够: 27 | 28 | https://github.com/shareAI-lab/mini_claude_code 29 | 30 | 教你假期闲的没事,搓一个值 3000 万美金的Agent AI 31 | 32 | 本期我们将会分享一个400行的Claude Code的超迷你代码(但已经能完成Claude Code的90%以上工作) 33 | 34 | 之后,我们将会陆续添加Todo 工具、Task(subagent)工具、System-reminder工具,并为你一一解开Claude Code 的所有功能特性设计疑惑。 35 | 36 | • 37 | 如果你不关心Claude Code的实现原理,可以直接使用Kode这个开源项目,Kode是响应ShareAI-lab在之前临时分享了Claude Code的逆向分析设计资料Repo后,应广大网友热切要求,顺便维护的开源版Claude Code(基于Cc早期逆向版本+逆向分析继续开发维护,并持续添加几乎所有Claude Code后续更新特性以及Codex的好用特性) 38 | 39 | 图片 40 | 41 | • 42 | 目前已被多个大型 / 明星初创商业公司的闭源产品参考 / 直接修改使用。 43 | 44 | 图片 45 | 图片 46 | 47 | 48 | • 49 | 欢迎 众多网友一起参与维护(也完全鼓励使用vibe Coding方式提PR),过去已有网友参与了Bash工具对Windows的支持、Docker环境添加、 WebSearch & WebFetch工具添加等,下个版本正在大面积重构中,正在添加内置Web、IDE插件,同时CLI包和Core SDK包解离。 50 | 51 | • 52 | Core SDK包支持在任意非cli场景用于各位开发者自己的业务场景下的灵活二次开发使用,让Claude Code 驱动你的下一个“世界上第一个 xx 领域 Manus 产品”运转。(当前SDK 的特性设计还没有完全收敛,我们将会在Kode群里组织腾讯会议进行方案讨论,欢迎一起参与) 53 | 54 | 图片 55 | 56 | 57 | • 58 | Kode - 开源版Claude Code:https://github.com/shareAI-lab/Kode 59 | 60 | • 61 | kimi 0905 搭配开源版Claude Code(kode)挺好用! 62 | 可以用开源 claude code (kode) + k2 0905 / deepseek v3.1 / gpt4.5 比闭源claude code + sonnet 降智的情况下好。 63 | 安装后配置自定义模型使用: 64 | npm install -g @shareai-lab/kode 65 | 66 | 大道至简, 万法归一 67 | 68 | 很多人看Claude Code 的逆向分析解读 / Kode 源码只见其形、不抓其神。我们重新出这个mini_claude_code系列仓库(包括之前的训练营)就是想把超出“术”的“道”进行传播和分享。 69 | 70 | 71 | 72 | 下面的代码推荐你复制到本地终端运行,随意修改、二开玩耍,这样你也搓了一个值 3000 万美金的agent AI项目(doge。 73 | 74 | 75 | 记住:对于下一代Agent 系统,模型是80%,代码是20%。 76 | 77 | #!/usr/bin/env python3 78 | import os 79 | import sys 80 | import json 81 | import time 82 | import threading 83 | import subprocess 84 | from pathlib import Path 85 | 86 | try: 87 | from anthropic import Anthropic 88 | except Exception as e: 89 | sys.stderr.write("Install with: pip install anthropic\n") 90 | raise 91 | 92 | ANTHROPIC_BASE_URL = "https://api.moonshot.cn/anthropic" 93 | ANTHROPIC_API_KEY = "sk-xxx" # 替换为你的key 94 | AGENT_MODEL = "kimi-k2-turbo-preview" 95 | 96 | # ---------- Workspace & Helpers ---------- 97 | WORKDIR = Path.cwd() 98 | MAX_TOOL_RESULT_CHARS = 100_000 99 | 100 | 101 | def safe_path(p: str) -> Path: 102 | abs_path = (WORKDIR / str(p or "")).resolve() 103 | rel = abs_path.relative_to(WORKDIR) if abs_path.is_relative_to(WORKDIR) else None 104 | if rel is None: 105 | raise ValueError("Path escapes workspace") 106 | return abs_path 107 | 108 | 109 | def clamp_text(s: str, n: int = MAX_TOOL_RESULT_CHARS) -> str: 110 | if len(s) <= n: 111 | return s 112 | return s[:n] + f"\n\n..." 113 | 114 | 115 | def pretty_tool_line(kind: str, title: str) -> None: 116 | print(f"⏺ {kind}({title})…") 117 | 118 | 119 | def pretty_sub_line(text: str) -> None: 120 | print(f" ⎿ {text}") 121 | 122 | 123 | # 轻量等待指示器(最小实现) 124 | class Spinner: 125 | def __init__(self, label: str = "等待模型响应") -> None: 126 | self.label = label 127 | self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] 128 | self._stop = threading.Event() 129 | self._thread = None 130 | 131 | def start(self): 132 | if not sys.stdout.isatty() or self._thread is not None: 133 | return 134 | self._stop.clear() 135 | 136 | def run(): 137 | i = 0 138 | while not self._stop.is_set(): 139 | sys.stdout.write("\r" + self.label + " " + self.frames[i % len(self.frames)]) 140 | sys.stdout.flush() 141 | i += 1 142 | time.sleep(0.08) 143 | 144 | self._thread = threading.Thread(target=run, daemon=True) 145 | self._thread.start() 146 | 147 | def stop(self): 148 | if self._thread is None: 149 | return 150 | self._stop.set() 151 | self._thread.join(timeout=1) 152 | self._thread = None 153 | try: 154 | # clear current line 155 | sys.stdout.write("\r\x1b[2K") 156 | sys.stdout.flush() 157 | except Exception: 158 | pass 159 | 160 | 161 | def log_error_debug(tag: str, info) -> None: 162 | try: 163 | js = json.dumps(info, ensure_ascii=False, indent=2) 164 | out = js if len(js) <= 4000 else js[:4000] + "\n..." 165 | print(f"⚠️ {tag}:") 166 | print(out) 167 | except Exception: 168 | print(f"⚠️ {tag}: (unserializable info)") 169 | 170 | 171 | # ---------- Content normalization helpers ---------- 172 | def block_to_dict(block): 173 | """Convert SDK response block objects to plain dicts for reuse in messages. 174 | Supports TextBlock, ToolUseBlock, and dict inputs. Best-effort fallback. 175 | """ 176 | if isinstance(block, dict): 177 | return block 178 | out = {} 179 | for key in ("type", "text", "id", "name", "input", "citations"): 180 | if hasattr(block, key): 181 | out[key] = getattr(block, key) 182 | # Fallback: include any public attributes 183 | if not out and hasattr(block, "__dict__"): 184 | out = {k: v for k, v in vars(block).items() if not k.startswith("_")} 185 | if hasattr(block, "type"): 186 | out["type"] = getattr(block, "type") 187 | return out 188 | 189 | 190 | def normalize_content_list(content): 191 | try: 192 | return [block_to_dict(b) for b in (content or [])] 193 | except Exception: 194 | return [] 195 | 196 | 197 | # ---------- SDK client ---------- 198 | api_key = ANTHROPIC_API_KEY 199 | if not api_key: 200 | sys.stderr.write("❌ ANTHROPIC_API_KEY not set\n") 201 | sys.exit(1) 202 | 203 | base_url = ANTHROPIC_BASE_URL 204 | client = Anthropic(api_key=api_key, base_url=base_url) if base_url else Anthropic(api_key=api_key) 205 | 206 | 207 | # ---------- System prompt ---------- 208 | SYSTEM = ( 209 | f"You are a coding agent operating INSIDE the user's repository at {WORKDIR}.\n" 210 | "Follow this loop strictly: plan briefly → use TOOLS to act directly on files/shell → report concise results.\n" 211 | "Rules:\n" 212 | "- Prefer taking actions with tools (read/write/edit/bash) over long prose.\n" 213 | "- Keep outputs terse. Use bullet lists / checklists when summarizing.\n" 214 | "- Never invent file paths. Ask via reads or list directories first if unsure.\n" 215 | "- For edits, apply the smallest change that satisfies the request.\n" 216 | "- For bash, avoid destructive or privileged commands; stay inside the workspace.\n" 217 | "- After finishing, summarize what changed and how to run or test." 218 | ) 219 | 220 | 221 | # ---------- Tools ---------- 222 | tools = [ 223 | { 224 | "name": "bash", 225 | "description": ( 226 | "Execute a shell command inside the project workspace. Use for scaffolding, " 227 | "formatting, running scripts, etc." 228 | ), 229 | "input_schema": { 230 | "type": "object", 231 | "properties": { 232 | "command": {"type": "string", "description": "Shell command to run"}, 233 | "timeout_ms": {"type": "integer", "minimum": 1000, "maximum": 120000}, 234 | }, 235 | "required": ["command"], 236 | "additionalProperties": False, 237 | }, 238 | }, 239 | { 240 | "name": "read_file", 241 | "description": "Read a UTF-8 text file. Optionally slice by line range or clamp length.", 242 | "input_schema": { 243 | "type": "object", 244 | "properties": { 245 | "path": {"type": "string"}, 246 | "start_line": {"type": "integer", "minimum": 1}, 247 | "end_line": {"type": "integer", "minimum": -1}, 248 | "max_chars": {"type": "integer", "minimum": 1, "maximum": 200000}, 249 | }, 250 | "required": ["path"], 251 | "additionalProperties": False, 252 | }, 253 | }, 254 | { 255 | "name": "write_file", 256 | "description": "Create or overwrite/append a UTF-8 text file. Use overwrite unless explicitly asked to append.", 257 | "input_schema": { 258 | "type": "object", 259 | "properties": { 260 | "path": {"type": "string"}, 261 | "content": {"type": "string"}, 262 | "mode": {"type": "string", "enum": ["overwrite", "append"], "default": "overwrite"}, 263 | }, 264 | "required": ["path", "content"], 265 | "additionalProperties": False, 266 | }, 267 | }, 268 | { 269 | "name": "edit_text", 270 | "description": "Small, precise text edits. Choose one action: replace | insert | delete_range.", 271 | "input_schema": { 272 | "type": "object", 273 | "properties": { 274 | "path": {"type": "string"}, 275 | "action": {"type": "string", "enum": ["replace", "insert", "delete_range"]}, 276 | "find": {"type": "string"}, 277 | "replace": {"type": "string"}, 278 | "insert_after": {"type": "integer", "minimum": -1}, 279 | "new_text": {"type": "string"}, 280 | "range": {"type": "array", "items": {"type": "integer"}, "minItems": 2, "maxItems": 2}, 281 | }, 282 | "required": ["path", "action"], 283 | "additionalProperties": False, 284 | }, 285 | }, 286 | ] 287 | 288 | 289 | # ---------- Tool executors ---------- 290 | def run_bash(input_obj: dict) -> str: 291 | cmd = str(input_obj.get("command") or "") 292 | if not cmd: 293 | raise ValueError("missing bash.command") 294 | if ( 295 | subprocess is not None 296 | and ("rm -rf /" in cmd or "shutdown" in cmd or "reboot" in cmd or "sudo " in cmd) 297 | ): 298 | raise ValueError("blocked dangerous command") 299 | timeout_ms = int(input_obj.get("timeout_ms") or 30000) 300 | try: 301 | proc = subprocess.run( 302 | cmd, 303 | cwd=str(WORKDIR), 304 | shell=True, 305 | capture_output=True, 306 | text=True, 307 | timeout=timeout_ms / 1000.0, 308 | ) 309 | out = "\n".join([x for x in [proc.stdout, proc.stderr] if x]).strip() 310 | return clamp_text(out or "(no output)") 311 | except subprocess.TimeoutExpired: 312 | return "(timeout)" 313 | 314 | 315 | def run_read(input_obj: dict) -> str: 316 | fp = safe_path(input_obj.get("path")) 317 | text = fp.read_text("utf-8") 318 | lines = text.split("\n") 319 | start = (max(1, int(input_obj.get("start_line") or 1)) - 1) if input_obj.get("start_line") else 0 320 | if isinstance(input_obj.get("end_line"), int): 321 | end_val = input_obj.get("end_line") 322 | end = len(lines) if end_val < 0 else max(start, end_val) 323 | else: 324 | end = len(lines) 325 | text = "\n".join(lines[start:end]) 326 | max_chars = int(input_obj.get("max_chars") or 100_000) 327 | return clamp_text(text, max_chars) 328 | 329 | 330 | def run_write(input_obj: dict) -> str: 331 | fp = safe_path(input_obj.get("path")) 332 | fp.parent.mkdir(parents=True, exist_ok=True) 333 | content = input_obj.get("content") or "" 334 | mode = input_obj.get("mode") 335 | if mode == "append" and fp.exists(): 336 | with fp.open("a", encoding="utf-8") as f: 337 | f.write(content) 338 | else: 339 | fp.write_text(content, encoding="utf-8") 340 | bytes_len = len(content.encode("utf-8")) 341 | rel = fp.relative_to(WORKDIR) 342 | return f"wrote {bytes_len} bytes to {rel}" 343 | 344 | 345 | def run_edit(input_obj: dict) -> str: 346 | fp = safe_path(input_obj.get("path")) 347 | text = fp.read_text("utf-8") 348 | action = input_obj.get("action") 349 | if action == "replace": 350 | find = str(input_obj.get("find") or "") 351 | if not find: 352 | raise ValueError("edit_text.replace missing find") 353 | replaced = text.replace(find, str(input_obj.get("replace") or "")) 354 | fp.write_text(replaced, encoding="utf-8") 355 | return f"replace done ({len(replaced.encode('utf-8'))} bytes)" 356 | elif action == "insert": 357 | line = int(input_obj.get("insert_after") if input_obj.get("insert_after") is not None else -1) 358 | lines = text.split("\n") 359 | idx = max(-1, min(len(lines) - 1, line)) 360 | lines[idx + 1:idx + 1] = [str(input_obj.get("new_text") or "")] 361 | nxt = "\n".join(lines) 362 | fp.write_text(nxt, encoding="utf-8") 363 | return f"inserted after line {line}" 364 | elif action == "delete_range": 365 | rng = input_obj.get("range") or [] 366 | if not (len(rng) == 2 and isinstance(rng[0], int) and isinstance(rng[1], int) and rng[1] >= rng[0]): 367 | raise ValueError("edit_text.delete_range invalid range") 368 | s, e = rng 369 | lines = text.split("\n") 370 | nxt = "\n".join([*lines[:s], *lines[e:]]) 371 | fp.write_text(nxt, encoding="utf-8") 372 | return f"deleted lines [{s}, {e})" 373 | else: 374 | raise ValueError(f"unsupported edit_text.action: {action}") 375 | 376 | 377 | def dispatch_tool(tu: dict) -> dict: 378 | try: 379 | # Support both dict and SDK block objects 380 | def gv(obj, key, default=None): 381 | return obj.get(key, default) if isinstance(obj, dict) else getattr(obj, key, default) 382 | 383 | name = gv(tu, "name") 384 | input_obj = gv(tu, "input", {}) or {} 385 | tool_use_id = gv(tu, "id") 386 | 387 | if name == "bash": 388 | pretty_tool_line("Bash", (input_obj.get("command") if isinstance(input_obj, dict) else None)) 389 | out = run_bash(input_obj if isinstance(input_obj, dict) else {}) 390 | pretty_sub_line(clamp_text(out, 2000) if out else "(No content)") 391 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 392 | if name == "read_file": 393 | pretty_tool_line("Read", (input_obj.get("path") if isinstance(input_obj, dict) else None)) 394 | out = run_read(input_obj if isinstance(input_obj, dict) else {}) 395 | pretty_sub_line(clamp_text(out, 2000)) 396 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 397 | if name == "write_file": 398 | pretty_tool_line("Write", (input_obj.get("path") if isinstance(input_obj, dict) else None)) 399 | out = run_write(input_obj if isinstance(input_obj, dict) else {}) 400 | pretty_sub_line(out) 401 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 402 | if name == "edit_text": 403 | action = input_obj.get("action") if isinstance(input_obj, dict) else None 404 | path_v = input_obj.get("path") if isinstance(input_obj, dict) else None 405 | pretty_tool_line("Edit", f"{action} {path_v}") 406 | out = run_edit(input_obj if isinstance(input_obj, dict) else {}) 407 | pretty_sub_line(out) 408 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 409 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": f"unknown tool: {name}", "is_error": True} 410 | except Exception as e: 411 | tool_use_id = tu.get("id") if isinstance(tu, dict) else getattr(tu, "id", None) 412 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": str(e), "is_error": True} 413 | 414 | 415 | # ---------- Core loop ---------- 416 | def query(messages: list, opts: dict | None = None) -> list: 417 | opts = opts or {} 418 | while True: 419 | spinner = Spinner() 420 | spinner.start() 421 | try: 422 | res = client.messages.create( 423 | model=AGENT_MODEL, 424 | system=SYSTEM, 425 | messages=messages, 426 | tools=tools, 427 | max_tokens=16000, 428 | **({"tool_choice": opts["tool_choice"]} if "tool_choice" in opts else {}), 429 | ) 430 | finally: 431 | spinner.stop() 432 | 433 | tool_uses = [] 434 | try: 435 | for block in getattr(res, "content", []): 436 | btype = getattr(block, "type", None) if not isinstance(block, dict) else block.get("type") 437 | if btype == "text": 438 | text = getattr(block, "text", None) if not isinstance(block, dict) else block.get("text") 439 | sys.stdout.write((text or "") + "\n") 440 | if btype == "tool_use": 441 | tool_uses.append(block) 442 | except Exception as err: 443 | log_error_debug( 444 | "Iterating res.content failed", 445 | { 446 | "error": str(err), 447 | "stop_reason": getattr(res, "stop_reason", None), 448 | "content_type": type(getattr(res, "content", None)).__name__, 449 | "is_array": isinstance(getattr(res, "content", None), list), 450 | "keys": list(res.__dict__.keys()) if hasattr(res, "__dict__") else [], 451 | "preview": (json.dumps(res, default=lambda o: getattr(o, "__dict__", str(o)))[:2000] if res else ""), 452 | }, 453 | ) 454 | raise 455 | 456 | if getattr(res, "stop_reason", None) == "tool_use": 457 | results = [dispatch_tool(tu) for tu in tool_uses] 458 | messages.append({"role": "assistant", "content": normalize_content_list(res.content)}) 459 | messages.append({"role": "user", "content": results}) 460 | continue 461 | 462 | messages.append({"role": "assistant", "content": normalize_content_list(res.content)}) 463 | return messages 464 | 465 | 466 | def main(): 467 | print(f"Tiny CC Agent (custom tools only) — cwd: {WORKDIR}") 468 | print('Type "exit" or "quit" to leave.\n') 469 | history: list = [] 470 | while True: 471 | try: 472 | line = input("User: ") 473 | except EOFError: 474 | break 475 | if not line or line.strip().lower() in {"q", "quit", "exit"}: 476 | break 477 | history.append({"role": "user", "content": [{"type": "text", "text": line}]}) 478 | try: 479 | query(history) 480 | except Exception as e: 481 | print("Error:", str(e)) 482 | 483 | 484 | if __name__ == "__main__": 485 | main() -------------------------------------------------------------------------------- /demo/js/particles.js: -------------------------------------------------------------------------------- 1 | // Particle effects for exhaust, dust, and collisions 2 | 3 | class ParticleSystem { 4 | constructor(scene) { 5 | this.scene = scene; 6 | this.particleGroups = new Map(); 7 | this.activeEffects = new Map(); 8 | this.maxParticles = 1000; 9 | this.particlePool = []; 10 | 11 | // Create particle pool 12 | this.createParticlePool(); 13 | } 14 | 15 | createParticlePool() { 16 | for (let i = 0; i < this.maxParticles; i++) { 17 | const particle = new Particle(); 18 | this.particlePool.push(particle); 19 | } 20 | } 21 | 22 | getParticle() { 23 | if (this.particlePool.length > 0) { 24 | return this.particlePool.pop(); 25 | } 26 | return new Particle(); 27 | } 28 | 29 | returnParticle(particle) { 30 | if (this.particlePool.length < this.maxParticles) { 31 | this.particlePool.push(particle); 32 | } 33 | } 34 | 35 | createExhaustEffect(position, velocity, intensity = 1) { 36 | const effect = new ExhaustEffect(this, position, velocity, intensity); 37 | this.activeEffects.set(`exhaust_${Date.now()}_${Math.random()}`, effect); 38 | return effect; 39 | } 40 | 41 | createDustEffect(position, velocity, intensity = 1) { 42 | const effect = new DustEffect(this, position, velocity, intensity); 43 | this.activeEffects.set(`dust_${Date.now()}_${Math.random()}`, effect); 44 | return effect; 45 | } 46 | 47 | createCollisionEffect(position, intensity = 1) { 48 | const effect = new CollisionEffect(this, position, intensity); 49 | this.activeEffects.set(`collision_${Date.now()}_${Math.random()}`, effect); 50 | return effect; 51 | } 52 | 53 | createSparksEffect(position, velocity, intensity = 1) { 54 | const effect = new SparksEffect(this, position, velocity, intensity); 55 | this.activeEffects.set(`sparks_${Date.now()}_${Math.random()}`, effect); 56 | return effect; 57 | } 58 | 59 | createBoostEffect(position, velocity, intensity = 1) { 60 | const effect = new BoostEffect(this, position, velocity, intensity); 61 | this.activeEffects.set(`boost_${Date.now()}_${Math.random()}`, effect); 62 | return effect; 63 | } 64 | 65 | update(deltaTime) { 66 | // Update all active effects 67 | this.activeEffects.forEach((effect, key) => { 68 | effect.update(deltaTime); 69 | 70 | if (effect.isFinished()) { 71 | effect.cleanup(); 72 | this.activeEffects.delete(key); 73 | } 74 | }); 75 | } 76 | 77 | cleanup() { 78 | this.activeEffects.forEach(effect => effect.cleanup()); 79 | this.activeEffects.clear(); 80 | } 81 | } 82 | 83 | class Particle { 84 | constructor() { 85 | this.position = new THREE.Vector3(); 86 | this.velocity = new THREE.Vector3(); 87 | this.acceleration = new THREE.Vector3(); 88 | this.life = 1.0; 89 | this.maxLife = 1.0; 90 | this.size = 1.0; 91 | this.color = new THREE.Color(1, 1, 1); 92 | this.alpha = 1.0; 93 | this.rotation = 0; 94 | this.rotationSpeed = 0; 95 | this.gravity = -9.8; 96 | this.drag = 0.98; 97 | this.active = false; 98 | 99 | // Visual representation 100 | this.mesh = null; 101 | } 102 | 103 | reset() { 104 | this.position.set(0, 0, 0); 105 | this.velocity.set(0, 0, 0); 106 | this.acceleration.set(0, 0, 0); 107 | this.life = 1.0; 108 | this.size = 1.0; 109 | this.color.set(1, 1, 1); 110 | this.alpha = 1.0; 111 | this.rotation = 0; 112 | this.rotationSpeed = 0; 113 | this.gravity = -9.8; 114 | this.drag = 0.98; 115 | this.active = false; 116 | 117 | if (this.mesh) { 118 | this.mesh.visible = false; 119 | } 120 | } 121 | 122 | update(deltaTime) { 123 | if (!this.active) return; 124 | 125 | // Update physics 126 | this.velocity.add(this.acceleration.clone().multiplyScalar(deltaTime)); 127 | this.velocity.multiplyScalar(this.drag); 128 | this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); 129 | 130 | // Apply gravity 131 | this.velocity.y += this.gravity * deltaTime; 132 | 133 | // Update life 134 | this.life -= deltaTime / this.maxLife; 135 | this.alpha = Math.max(0, this.life); 136 | 137 | // Update rotation 138 | this.rotation += this.rotationSpeed * deltaTime; 139 | 140 | // Update visual 141 | if (this.mesh) { 142 | this.mesh.position.copy(this.position); 143 | this.mesh.rotation.z = this.rotation; 144 | this.mesh.material.opacity = this.alpha; 145 | this.mesh.scale.setScalar(this.size); 146 | } 147 | 148 | // Deactivate if life is over 149 | if (this.life <= 0) { 150 | this.active = false; 151 | } 152 | } 153 | 154 | isActive() { 155 | return this.active; 156 | } 157 | 158 | activate(position, velocity, color, size = 1.0, life = 1.0) { 159 | this.position.copy(position); 160 | this.velocity.copy(velocity); 161 | this.acceleration.set(0, 0, 0); 162 | this.color.copy(color); 163 | this.size = size; 164 | this.life = life; 165 | this.maxLife = life; 166 | this.alpha = 1.0; 167 | this.active = true; 168 | 169 | if (this.mesh) { 170 | this.mesh.visible = true; 171 | this.mesh.material.color.copy(color); 172 | } 173 | } 174 | } 175 | 176 | class ParticleEffect { 177 | constructor(particleSystem, position, intensity = 1) { 178 | this.particleSystem = particleSystem; 179 | this.position = position.clone(); 180 | this.intensity = intensity; 181 | this.particles = []; 182 | this.emissionRate = 50; 183 | this.emissionTimer = 0; 184 | this.duration = 2.0; 185 | this.age = 0; 186 | this.finished = false; 187 | } 188 | 189 | update(deltaTime) { 190 | this.age += deltaTime; 191 | this.emissionTimer += deltaTime; 192 | 193 | // Emit particles 194 | while (this.emissionTimer >= 1 / this.emissionRate && !this.finished) { 195 | this.emitParticle(); 196 | this.emissionTimer -= 1 / this.emissionRate; 197 | } 198 | 199 | // Update existing particles 200 | this.particles.forEach((particle, index) => { 201 | particle.update(deltaTime); 202 | 203 | if (!particle.isActive()) { 204 | this.particles.splice(index, 1); 205 | this.particleSystem.returnParticle(particle); 206 | } 207 | }); 208 | 209 | // Check if effect should finish 210 | if (this.age >= this.duration && this.particles.length === 0) { 211 | this.finished = true; 212 | } 213 | } 214 | 215 | emitParticle() { 216 | const particle = this.particleSystem.getParticle(); 217 | const velocity = this.getParticleVelocity(); 218 | const color = this.getParticleColor(); 219 | const size = this.getParticleSize(); 220 | const life = this.getParticleLife(); 221 | 222 | particle.activate(this.position, velocity, color, size, life); 223 | this.particles.push(particle); 224 | } 225 | 226 | getParticleVelocity() { 227 | return new THREE.Vector3( 228 | (Math.random() - 0.5) * 2, 229 | Math.random() * 2, 230 | (Math.random() - 0.5) * 2 231 | ).multiplyScalar(this.intensity); 232 | } 233 | 234 | getParticleColor() { 235 | return new THREE.Color(1, 1, 1); 236 | } 237 | 238 | getParticleSize() { 239 | return 0.1; 240 | } 241 | 242 | getParticleLife() { 243 | return 1.0; 244 | } 245 | 246 | isFinished() { 247 | return this.finished; 248 | } 249 | 250 | cleanup() { 251 | this.particles.forEach(particle => { 252 | this.particleSystem.returnParticle(particle); 253 | }); 254 | this.particles = []; 255 | } 256 | } 257 | 258 | class ExhaustEffect extends ParticleEffect { 259 | constructor(particleSystem, position, velocity, intensity) { 260 | super(particleSystem, position, intensity); 261 | this.emissionRate = 100 * intensity; 262 | this.duration = 0.5; 263 | this.velocity = velocity.clone(); 264 | } 265 | 266 | getParticleVelocity() { 267 | return new THREE.Vector3( 268 | (Math.random() - 0.5) * 0.5, 269 | Math.random() * 0.5, 270 | (Math.random() - 0.5) * 0.5 271 | ).add(this.velocity.clone().multiplyScalar(-0.2)); 272 | } 273 | 274 | getParticleColor() { 275 | const gray = MathUtils.randomRange(0.2, 0.8); 276 | return new THREE.Color(gray, gray, gray); 277 | } 278 | 279 | getParticleSize() { 280 | return MathUtils.randomRange(0.05, 0.15); 281 | } 282 | 283 | getParticleLife() { 284 | return MathUtils.randomRange(0.5, 1.5); 285 | } 286 | } 287 | 288 | class DustEffect extends ParticleEffect { 289 | constructor(particleSystem, position, velocity, intensity) { 290 | super(particleSystem, position, intensity); 291 | this.emissionRate = 80 * intensity; 292 | this.duration = 1.0; 293 | this.velocity = velocity.clone(); 294 | } 295 | 296 | getParticleVelocity() { 297 | return new THREE.Vector3( 298 | (Math.random() - 0.5) * 3, 299 | Math.random() * 1, 300 | (Math.random() - 0.5) * 3 301 | ).add(this.velocity.clone().multiplyScalar(0.1)); 302 | } 303 | 304 | getParticleColor() { 305 | const brown = MathUtils.randomRange(0.4, 0.7); 306 | return new THREE.Color(brown, brown * 0.8, brown * 0.6); 307 | } 308 | 309 | getParticleSize() { 310 | return MathUtils.randomRange(0.1, 0.3); 311 | } 312 | 313 | getParticleLife() { 314 | return MathUtils.randomRange(1.0, 2.0); 315 | } 316 | } 317 | 318 | class CollisionEffect extends ParticleEffect { 319 | constructor(particleSystem, position, intensity) { 320 | super(particleSystem, position, intensity); 321 | this.emissionRate = 200 * intensity; 322 | this.duration = 0.2; 323 | this.explosive = true; 324 | } 325 | 326 | getParticleVelocity() { 327 | const speed = MathUtils.randomRange(2, 8) * this.intensity; 328 | const direction = new THREE.Vector3( 329 | (Math.random() - 0.5) * 2, 330 | Math.random(), 331 | (Math.random() - 0.5) * 2 332 | ).normalize(); 333 | 334 | return direction.multiplyScalar(speed); 335 | } 336 | 337 | getParticleColor() { 338 | const colors = [ 339 | new THREE.Color(1, 0.5, 0), // Orange 340 | new THREE.Color(1, 1, 0), // Yellow 341 | new THREE.Color(1, 0, 0), // Red 342 | new THREE.Color(0.5, 0.5, 0.5) // Gray 343 | ]; 344 | return colors[Math.floor(Math.random() * colors.length)]; 345 | } 346 | 347 | getParticleSize() { 348 | return MathUtils.randomRange(0.05, 0.2) * this.intensity; 349 | } 350 | 351 | getParticleLife() { 352 | return MathUtils.randomRange(0.3, 1.0); 353 | } 354 | } 355 | 356 | class SparksEffect extends ParticleEffect { 357 | constructor(particleSystem, position, velocity, intensity) { 358 | super(particleSystem, position, intensity); 359 | this.emissionRate = 150 * intensity; 360 | this.duration = 0.3; 361 | this.velocity = velocity.clone(); 362 | } 363 | 364 | getParticleVelocity() { 365 | const speed = MathUtils.randomRange(1, 5) * this.intensity; 366 | const direction = new THREE.Vector3( 367 | (Math.random() - 0.5) * 2, 368 | Math.random() * 0.5, 369 | (Math.random() - 0.5) * 2 370 | ).normalize(); 371 | 372 | return direction.multiplyScalar(speed).add(this.velocity.clone().multiplyScalar(0.2)); 373 | } 374 | 375 | getParticleColor() { 376 | return new THREE.Color(1, 0.8, 0.2); // Yellow-orange sparks 377 | } 378 | 379 | getParticleSize() { 380 | return MathUtils.randomRange(0.02, 0.08); 381 | } 382 | 383 | getParticleLife() { 384 | return MathUtils.randomRange(0.2, 0.6); 385 | } 386 | } 387 | 388 | class BoostEffect extends ParticleEffect { 389 | constructor(particleSystem, position, velocity, intensity) { 390 | super(particleSystem, position, intensity); 391 | this.emissionRate = 300 * intensity; 392 | this.duration = 0.8; 393 | this.velocity = velocity.clone(); 394 | } 395 | 396 | getParticleVelocity() { 397 | return new THREE.Vector3( 398 | (Math.random() - 0.5) * 1, 399 | Math.random() * 0.5, 400 | (Math.random() - 0.5) * 1 401 | ).add(this.velocity.clone().multiplyScalar(-0.3)); 402 | } 403 | 404 | getParticleColor() { 405 | const colors = [ 406 | new THREE.Color(0, 0.5, 1), // Blue 407 | new THREE.Color(0, 1, 1), // Cyan 408 | new THREE.Color(0.5, 1, 1), // Light blue 409 | new THREE.Color(1, 1, 1) // White 410 | ]; 411 | return colors[Math.floor(Math.random() * colors.length)]; 412 | } 413 | 414 | getParticleSize() { 415 | return MathUtils.randomRange(0.03, 0.12); 416 | } 417 | 418 | getParticleLife() { 419 | return MathUtils.randomRange(0.4, 1.2); 420 | } 421 | } 422 | 423 | // Particle system integration for cars 424 | class CarParticleEffects { 425 | constructor(particleSystem, car) { 426 | this.particleSystem = particleSystem; 427 | this.car = car; 428 | this.exhaustEffects = []; 429 | this.dustEffects = []; 430 | this.collisionEffects = []; 431 | 432 | this.exhaustTimer = 0; 433 | this.dustTimer = 0; 434 | this.lastCollisionTime = 0; 435 | 436 | this.setupExhaustPoints(); 437 | } 438 | 439 | setupExhaustPoints() { 440 | this.exhaustPoints = [ 441 | new THREE.Vector3(-1.5, -0.3, -4.2), 442 | new THREE.Vector3(1.5, -0.3, -4.2) 443 | ]; 444 | } 445 | 446 | update(deltaTime) { 447 | const carPosition = this.car.getPosition(); 448 | const carVelocity = this.car.physicsBody ? this.car.physicsBody.getVelocity() : new THREE.Vector3(); 449 | const speed = this.car.getSpeed(); 450 | const throttle = this.car.throttle; 451 | 452 | // Update exhaust effects 453 | this.updateExhaustEffects(carPosition, carVelocity, throttle, deltaTime); 454 | 455 | // Update dust effects 456 | this.updateDustEffects(carPosition, carVelocity, speed, deltaTime); 457 | 458 | // Check for collisions 459 | this.checkCollisions(carPosition, deltaTime); 460 | } 461 | 462 | updateExhaustEffects(carPosition, carVelocity, throttle, deltaTime) { 463 | this.exhaustTimer += deltaTime; 464 | 465 | if (throttle > 0.1 && this.exhaustTimer >= 0.02) { 466 | this.exhaustPoints.forEach(point => { 467 | const worldPos = point.clone().applyQuaternion(this.car.getRotation()).add(carPosition); 468 | const effect = this.particleSystem.createExhaustEffect(worldPos, carVelocity, throttle); 469 | this.exhaustEffects.push(effect); 470 | }); 471 | 472 | this.exhaustTimer = 0; 473 | } 474 | 475 | // Clean up old effects 476 | this.exhaustEffects = this.exhaustEffects.filter(effect => !effect.isFinished()); 477 | } 478 | 479 | updateDustEffects(carPosition, carVelocity, speed, deltaTime) { 480 | if (speed > 20) { 481 | this.dustTimer += deltaTime; 482 | 483 | if (this.dustTimer >= 0.1) { 484 | // Create dust from wheels 485 | const wheelPositions = [ 486 | new THREE.Vector3(-1.5, -0.8, 2.5), 487 | new THREE.Vector3(1.5, -0.8, 2.5), 488 | new THREE.Vector3(-1.5, -0.8, -2.5), 489 | new THREE.Vector3(1.5, -0.8, -2.5) 490 | ]; 491 | 492 | wheelPositions.forEach(pos => { 493 | const worldPos = pos.clone().applyQuaternion(this.car.getRotation()).add(carPosition); 494 | const effect = this.particleSystem.createDustEffect(worldPos, carVelocity, speed / 100); 495 | this.dustEffects.push(effect); 496 | }); 497 | 498 | this.dustTimer = 0; 499 | } 500 | } 501 | 502 | // Clean up old effects 503 | this.dustEffects = this.dustEffects.filter(effect => !effect.isFinished()); 504 | } 505 | 506 | checkCollisions(carPosition, deltaTime) { 507 | // This would be called from collision detection system 508 | // For now, just a placeholder 509 | } 510 | 511 | createCollisionEffect(position, intensity = 1) { 512 | const effect = this.particleSystem.createCollisionEffect(position, intensity); 513 | this.collisionEffects.push(effect); 514 | return effect; 515 | } 516 | 517 | createSparksEffect(position, velocity, intensity = 1) { 518 | const effect = this.particleSystem.createSparksEffect(position, velocity, intensity); 519 | this.collisionEffects.push(effect); 520 | return effect; 521 | } 522 | 523 | createBoostEffect(position, velocity, intensity = 1) { 524 | const effect = this.particleSystem.createBoostEffect(position, velocity, intensity); 525 | this.collisionEffects.push(effect); 526 | return effect; 527 | } 528 | 529 | cleanup() { 530 | this.exhaustEffects.forEach(effect => effect.cleanup()); 531 | this.dustEffects.forEach(effect => effect.cleanup()); 532 | this.collisionEffects.forEach(effect => effect.cleanup()); 533 | 534 | this.exhaustEffects = []; 535 | this.dustEffects = []; 536 | this.collisionEffects = []; 537 | } 538 | } 539 | 540 | // Export for use in other modules 541 | if (typeof module !== 'undefined' && module.exports) { 542 | module.exports = { 543 | ParticleSystem, 544 | Particle, 545 | ParticleEffect, 546 | CarParticleEffects 547 | }; 548 | } -------------------------------------------------------------------------------- /v1_basic_agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import json 5 | import re 6 | import time 7 | import threading 8 | import subprocess 9 | from pathlib import Path 10 | 11 | try: 12 | from anthropic import Anthropic 13 | except Exception as e: 14 | sys.stderr.write("Install with: pip install anthropic\n") 15 | raise 16 | 17 | ANTHROPIC_BASE_URL = "https://api.moonshot.cn/anthropic" 18 | ANTHROPIC_API_KEY = "sk-xxx" # Replace with your API key 19 | AGENT_MODEL = "kimi-k2-turbo-preview" 20 | 21 | # ---------- Workspace & Helpers ---------- 22 | WORKDIR = Path.cwd() 23 | MAX_TOOL_RESULT_CHARS = 100_000 24 | 25 | RESET = "\x1b[0m" 26 | PRIMARY_COLOR = "\x1b[38;2;120;200;255m" 27 | ACCENT_COLOR = "\x1b[38;2;150;140;255m" 28 | INFO_COLOR = "\x1b[38;2;110;110;110m" 29 | PROMPT_COLOR = "\x1b[38;2;120;200;255m" 30 | DIVIDER = "\n" 31 | 32 | 33 | MD_BOLD = re.compile(r"\*\*(.+?)\*\*") 34 | MD_CODE = re.compile(r"`([^`]+)`") 35 | MD_HEADING = re.compile(r"^(#{1,6})\s*(.+)$", re.MULTILINE) 36 | MD_BULLET = re.compile(r"^\s*[-\*]\s+", re.MULTILINE) 37 | 38 | 39 | def clear_screen() -> None: 40 | if sys.stdout.isatty(): 41 | sys.stdout.write("\033c") 42 | sys.stdout.flush() 43 | 44 | 45 | def render_banner(title: str, subtitle: str | None = None) -> None: 46 | print(f"{PRIMARY_COLOR}{title}{RESET}") 47 | if subtitle: 48 | print(f"{ACCENT_COLOR}{subtitle}{RESET}") 49 | print() 50 | 51 | 52 | def user_prompt_label() -> str: 53 | return f"{ACCENT_COLOR}{RESET} {PROMPT_COLOR}User{RESET}{INFO_COLOR} >> {RESET}" 54 | 55 | 56 | def print_divider() -> None: 57 | print(DIVIDER, end="") 58 | 59 | 60 | def format_markdown(text: str) -> str: 61 | if not text or text.lstrip().startswith("\x1b"): 62 | return text 63 | 64 | def bold_repl(match: re.Match[str]) -> str: 65 | return f"\x1b[1m{match.group(1)}\x1b[0m" 66 | 67 | def code_repl(match: re.Match[str]) -> str: 68 | return f"\x1b[38;2;255;214;102m{match.group(1)}\x1b[0m" 69 | 70 | def heading_repl(match: re.Match[str]) -> str: 71 | return f"\x1b[1m{match.group(2)}\x1b[0m" 72 | 73 | formatted = MD_BOLD.sub(bold_repl, text) 74 | formatted = MD_CODE.sub(code_repl, formatted) 75 | formatted = MD_HEADING.sub(heading_repl, formatted) 76 | formatted = MD_BULLET.sub("• ", formatted) 77 | return formatted 78 | 79 | 80 | def safe_path(p: str) -> Path: 81 | abs_path = (WORKDIR / str(p or "")).resolve() 82 | rel = abs_path.relative_to(WORKDIR) if abs_path.is_relative_to(WORKDIR) else None 83 | if rel is None: 84 | raise ValueError("Path escapes workspace") 85 | return abs_path 86 | 87 | 88 | def clamp_text(s: str, n: int = MAX_TOOL_RESULT_CHARS) -> str: 89 | if len(s) <= n: 90 | return s 91 | return s[:n] + f"\n\n..." 92 | 93 | 94 | def pretty_tool_line(kind: str, title: str | None) -> None: 95 | body = f"{kind}({title})…" if title else kind 96 | glow = f"{ACCENT_COLOR}\x1b[1m" 97 | print(f"{glow}⏺ {body}{RESET}") 98 | 99 | 100 | def pretty_sub_line(text: str) -> None: 101 | lines = text.splitlines() or [""] 102 | for line in lines: 103 | print(f" ⎿ {format_markdown(line)}") 104 | 105 | 106 | # Minimal spinner for model waits 107 | class Spinner: 108 | def __init__(self, label: str = "Waiting for model") -> None: 109 | self.label = label 110 | self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] 111 | self.color = "\x1b[38;2;255;229;92m" 112 | self._stop = threading.Event() 113 | self._thread = None 114 | 115 | def start(self): 116 | if not sys.stdout.isatty() or self._thread is not None: 117 | return 118 | self._stop.clear() 119 | 120 | def run(): 121 | start_ts = time.time() 122 | index = 0 123 | while not self._stop.is_set(): 124 | elapsed = time.time() - start_ts 125 | frame = self.frames[index % len(self.frames)] 126 | styled = f"{self.color}{frame} {self.label} ({elapsed:.1f}s)\x1b[0m" 127 | sys.stdout.write("\r" + styled) 128 | sys.stdout.flush() 129 | index += 1 130 | time.sleep(0.08) 131 | 132 | self._thread = threading.Thread(target=run, daemon=True) 133 | self._thread.start() 134 | 135 | def stop(self): 136 | if self._thread is None: 137 | return 138 | self._stop.set() 139 | self._thread.join(timeout=1) 140 | self._thread = None 141 | try: 142 | # clear current line 143 | sys.stdout.write("\r\x1b[2K") 144 | sys.stdout.flush() 145 | except Exception: 146 | pass 147 | 148 | 149 | def log_error_debug(tag: str, info) -> None: 150 | try: 151 | js = json.dumps(info, ensure_ascii=False, indent=2) 152 | out = js if len(js) <= 4000 else js[:4000] + "\n..." 153 | print(f"⚠️ {tag}:") 154 | print(out) 155 | except Exception: 156 | print(f"⚠️ {tag}: (unserializable info)") 157 | 158 | 159 | # ---------- Content normalization helpers ---------- 160 | def block_to_dict(block): 161 | """Convert SDK response block objects to plain dicts for reuse in messages. 162 | Supports TextBlock, ToolUseBlock, and dict inputs. Best-effort fallback. 163 | """ 164 | if isinstance(block, dict): 165 | return block 166 | out = {} 167 | for key in ("type", "text", "id", "name", "input", "citations"): 168 | if hasattr(block, key): 169 | out[key] = getattr(block, key) 170 | # Fallback: include any public attributes 171 | if not out and hasattr(block, "__dict__"): 172 | out = {k: v for k, v in vars(block).items() if not k.startswith("_")} 173 | if hasattr(block, "type"): 174 | out["type"] = getattr(block, "type") 175 | return out 176 | 177 | 178 | def normalize_content_list(content): 179 | try: 180 | return [block_to_dict(b) for b in (content or [])] 181 | except Exception: 182 | return [] 183 | 184 | 185 | # ---------- SDK client ---------- 186 | api_key = ANTHROPIC_API_KEY 187 | if not api_key: 188 | sys.stderr.write("❌ ANTHROPIC_API_KEY not set\n") 189 | sys.exit(1) 190 | 191 | base_url = ANTHROPIC_BASE_URL 192 | client = Anthropic(api_key=api_key, base_url=base_url) if base_url else Anthropic(api_key=api_key) 193 | 194 | 195 | # ---------- System prompt ---------- 196 | SYSTEM = ( 197 | f"You are a coding agent operating INSIDE the user's repository at {WORKDIR}.\n" 198 | "Follow this loop strictly: plan briefly → use TOOLS to act directly on files/shell → report concise results.\n" 199 | "Rules:\n" 200 | "- Prefer taking actions with tools (read/write/edit/bash) over long prose.\n" 201 | "- Keep outputs terse. Use bullet lists / checklists when summarizing.\n" 202 | "- Never invent file paths. Ask via reads or list directories first if unsure.\n" 203 | "- For edits, apply the smallest change that satisfies the request.\n" 204 | "- For bash, avoid destructive or privileged commands; stay inside the workspace.\n" 205 | "- After finishing, summarize what changed and how to run or test." 206 | ) 207 | 208 | 209 | # ---------- Tools ---------- 210 | tools = [ 211 | { 212 | "name": "bash", 213 | "description": ( 214 | "Execute a shell command inside the project workspace. Use for scaffolding, " 215 | "formatting, running scripts, etc." 216 | ), 217 | "input_schema": { 218 | "type": "object", 219 | "properties": { 220 | "command": {"type": "string", "description": "Shell command to run"}, 221 | "timeout_ms": {"type": "integer", "minimum": 1000, "maximum": 120000}, 222 | }, 223 | "required": ["command"], 224 | "additionalProperties": False, 225 | }, 226 | }, 227 | { 228 | "name": "read_file", 229 | "description": "Read a UTF-8 text file. Optionally slice by line range or clamp length.", 230 | "input_schema": { 231 | "type": "object", 232 | "properties": { 233 | "path": {"type": "string"}, 234 | "start_line": {"type": "integer", "minimum": 1}, 235 | "end_line": {"type": "integer", "minimum": -1}, 236 | "max_chars": {"type": "integer", "minimum": 1, "maximum": 200000}, 237 | }, 238 | "required": ["path"], 239 | "additionalProperties": False, 240 | }, 241 | }, 242 | { 243 | "name": "write_file", 244 | "description": "Create or overwrite/append a UTF-8 text file. Use overwrite unless explicitly asked to append.", 245 | "input_schema": { 246 | "type": "object", 247 | "properties": { 248 | "path": {"type": "string"}, 249 | "content": {"type": "string"}, 250 | "mode": {"type": "string", "enum": ["overwrite", "append"], "default": "overwrite"}, 251 | }, 252 | "required": ["path", "content"], 253 | "additionalProperties": False, 254 | }, 255 | }, 256 | { 257 | "name": "edit_text", 258 | "description": "Small, precise text edits. Choose one action: replace | insert | delete_range.", 259 | "input_schema": { 260 | "type": "object", 261 | "properties": { 262 | "path": {"type": "string"}, 263 | "action": {"type": "string", "enum": ["replace", "insert", "delete_range"]}, 264 | "find": {"type": "string"}, 265 | "replace": {"type": "string"}, 266 | "insert_after": {"type": "integer", "minimum": -1}, 267 | "new_text": {"type": "string"}, 268 | "range": {"type": "array", "items": {"type": "integer"}, "minItems": 2, "maxItems": 2}, 269 | }, 270 | "required": ["path", "action"], 271 | "additionalProperties": False, 272 | }, 273 | }, 274 | ] 275 | 276 | 277 | # ---------- Tool executors ---------- 278 | def run_bash(input_obj: dict) -> str: 279 | cmd = str(input_obj.get("command") or "") 280 | if not cmd: 281 | raise ValueError("missing bash.command") 282 | if ( 283 | subprocess is not None 284 | and ("rm -rf /" in cmd or "shutdown" in cmd or "reboot" in cmd or "sudo " in cmd) 285 | ): 286 | raise ValueError("blocked dangerous command") 287 | timeout_ms = int(input_obj.get("timeout_ms") or 30000) 288 | try: 289 | proc = subprocess.run( 290 | cmd, 291 | cwd=str(WORKDIR), 292 | shell=True, 293 | capture_output=True, 294 | text=True, 295 | timeout=timeout_ms / 1000.0, 296 | ) 297 | out = "\n".join([x for x in [proc.stdout, proc.stderr] if x]).strip() 298 | return clamp_text(out or "(no output)") 299 | except subprocess.TimeoutExpired: 300 | return "(timeout)" 301 | 302 | 303 | def run_read(input_obj: dict) -> str: 304 | fp = safe_path(input_obj.get("path")) 305 | text = fp.read_text("utf-8") 306 | lines = text.split("\n") 307 | start = (max(1, int(input_obj.get("start_line") or 1)) - 1) if input_obj.get("start_line") else 0 308 | if isinstance(input_obj.get("end_line"), int): 309 | end_val = input_obj.get("end_line") 310 | end = len(lines) if end_val < 0 else max(start, end_val) 311 | else: 312 | end = len(lines) 313 | text = "\n".join(lines[start:end]) 314 | max_chars = int(input_obj.get("max_chars") or 100_000) 315 | return clamp_text(text, max_chars) 316 | 317 | 318 | def run_write(input_obj: dict) -> str: 319 | fp = safe_path(input_obj.get("path")) 320 | fp.parent.mkdir(parents=True, exist_ok=True) 321 | content = input_obj.get("content") or "" 322 | mode = input_obj.get("mode") 323 | if mode == "append" and fp.exists(): 324 | with fp.open("a", encoding="utf-8") as f: 325 | f.write(content) 326 | else: 327 | fp.write_text(content, encoding="utf-8") 328 | bytes_len = len(content.encode("utf-8")) 329 | rel = fp.relative_to(WORKDIR) 330 | return f"wrote {bytes_len} bytes to {rel}" 331 | 332 | 333 | def run_edit(input_obj: dict) -> str: 334 | fp = safe_path(input_obj.get("path")) 335 | text = fp.read_text("utf-8") 336 | action = input_obj.get("action") 337 | if action == "replace": 338 | find = str(input_obj.get("find") or "") 339 | if not find: 340 | raise ValueError("edit_text.replace missing find") 341 | replaced = text.replace(find, str(input_obj.get("replace") or "")) 342 | fp.write_text(replaced, encoding="utf-8") 343 | return f"replace done ({len(replaced.encode('utf-8'))} bytes)" 344 | elif action == "insert": 345 | line = int(input_obj.get("insert_after") if input_obj.get("insert_after") is not None else -1) 346 | lines = text.split("\n") 347 | idx = max(-1, min(len(lines) - 1, line)) 348 | lines[idx + 1:idx + 1] = [str(input_obj.get("new_text") or "")] 349 | nxt = "\n".join(lines) 350 | fp.write_text(nxt, encoding="utf-8") 351 | return f"inserted after line {line}" 352 | elif action == "delete_range": 353 | rng = input_obj.get("range") or [] 354 | if not (len(rng) == 2 and isinstance(rng[0], int) and isinstance(rng[1], int) and rng[1] >= rng[0]): 355 | raise ValueError("edit_text.delete_range invalid range") 356 | s, e = rng 357 | lines = text.split("\n") 358 | nxt = "\n".join([*lines[:s], *lines[e:]]) 359 | fp.write_text(nxt, encoding="utf-8") 360 | return f"deleted lines [{s}, {e})" 361 | else: 362 | raise ValueError(f"unsupported edit_text.action: {action}") 363 | 364 | 365 | def dispatch_tool(tu: dict) -> dict: 366 | try: 367 | # Support both dict and SDK block objects 368 | def gv(obj, key, default=None): 369 | return obj.get(key, default) if isinstance(obj, dict) else getattr(obj, key, default) 370 | 371 | name = gv(tu, "name") 372 | input_obj = gv(tu, "input", {}) or {} 373 | tool_use_id = gv(tu, "id") 374 | 375 | if name == "bash": 376 | pretty_tool_line("Bash", (input_obj.get("command") if isinstance(input_obj, dict) else None)) 377 | out = run_bash(input_obj if isinstance(input_obj, dict) else {}) 378 | pretty_sub_line(clamp_text(out, 2000) if out else "(No content)") 379 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 380 | if name == "read_file": 381 | pretty_tool_line("Read", (input_obj.get("path") if isinstance(input_obj, dict) else None)) 382 | out = run_read(input_obj if isinstance(input_obj, dict) else {}) 383 | pretty_sub_line(clamp_text(out, 2000)) 384 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 385 | if name == "write_file": 386 | pretty_tool_line("Write", (input_obj.get("path") if isinstance(input_obj, dict) else None)) 387 | out = run_write(input_obj if isinstance(input_obj, dict) else {}) 388 | pretty_sub_line(out) 389 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 390 | if name == "edit_text": 391 | action = input_obj.get("action") if isinstance(input_obj, dict) else None 392 | path_v = input_obj.get("path") if isinstance(input_obj, dict) else None 393 | pretty_tool_line("Edit", f"{action} {path_v}") 394 | out = run_edit(input_obj if isinstance(input_obj, dict) else {}) 395 | pretty_sub_line(out) 396 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": out} 397 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": f"unknown tool: {name}", "is_error": True} 398 | except Exception as e: 399 | tool_use_id = tu.get("id") if isinstance(tu, dict) else getattr(tu, "id", None) 400 | return {"type": "tool_result", "tool_use_id": tool_use_id, "content": str(e), "is_error": True} 401 | 402 | 403 | # ---------- Core loop ---------- 404 | def query(messages: list, opts: dict | None = None) -> list: 405 | opts = opts or {} 406 | while True: 407 | spinner = Spinner() 408 | spinner.start() 409 | try: 410 | res = client.messages.create( 411 | model=AGENT_MODEL, 412 | system=SYSTEM, 413 | messages=messages, 414 | tools=tools, 415 | max_tokens=16000, 416 | **({"tool_choice": opts["tool_choice"]} if "tool_choice" in opts else {}), 417 | ) 418 | finally: 419 | spinner.stop() 420 | 421 | tool_uses = [] 422 | try: 423 | for block in getattr(res, "content", []): 424 | btype = getattr(block, "type", None) if not isinstance(block, dict) else block.get("type") 425 | if btype == "text": 426 | text = getattr(block, "text", None) if not isinstance(block, dict) else block.get("text") 427 | sys.stdout.write(format_markdown(text or "") + "\n") 428 | if btype == "tool_use": 429 | tool_uses.append(block) 430 | except Exception as err: 431 | log_error_debug( 432 | "Iterating res.content failed", 433 | { 434 | "error": str(err), 435 | "stop_reason": getattr(res, "stop_reason", None), 436 | "content_type": type(getattr(res, "content", None)).__name__, 437 | "is_array": isinstance(getattr(res, "content", None), list), 438 | "keys": list(res.__dict__.keys()) if hasattr(res, "__dict__") else [], 439 | "preview": (json.dumps(res, default=lambda o: getattr(o, "__dict__", str(o)))[:2000] if res else ""), 440 | }, 441 | ) 442 | raise 443 | 444 | if getattr(res, "stop_reason", None) == "tool_use": 445 | results = [dispatch_tool(tu) for tu in tool_uses] 446 | messages.append({"role": "assistant", "content": normalize_content_list(res.content)}) 447 | messages.append({"role": "user", "content": results}) 448 | continue 449 | 450 | messages.append({"role": "assistant", "content": normalize_content_list(res.content)}) 451 | return messages 452 | 453 | 454 | def main(): 455 | clear_screen() 456 | render_banner("Tiny Kode Agent", "custom tools only") 457 | print(f"{INFO_COLOR}Workspace: {WORKDIR}{RESET}") 458 | print(f"{INFO_COLOR}Type \"exit\" or \"quit\" to leave.{RESET}\n") 459 | history: list = [] 460 | while True: 461 | try: 462 | line = input(user_prompt_label()) 463 | except EOFError: 464 | break 465 | if not line or line.strip().lower() in {"q", "quit", "exit"}: 466 | break 467 | print_divider() 468 | history.append({"role": "user", "content": [{"type": "text", "text": line}]}) 469 | try: 470 | query(history) 471 | except Exception as e: 472 | print(f"{ACCENT_COLOR}Error{RESET}: {str(e)}") 473 | 474 | 475 | if __name__ == "__main__": 476 | main() 477 | --------------------------------------------------------------------------------