├── 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 |
75 |
速度: 0 km/h
76 |
圈数: 0/3
77 |
时间: 0s
78 |
79 |
80 |
81 |
控制说明
82 |
↑ - 加速 | ↓ - 刹车 | ← → - 转向
83 |
空格 - 手刹 | R - 重置位置
84 |
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 |
5 | Fellow us on X: https://x.com/baicai003
6 |
7 |
8 |
9 |
10 |
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 |
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 |
--------------------------------------------------------------------------------