TeleCoder3-36B-Thinking
Browse files- .gitattributes +2 -0
- Demo/五子棋.gif +3 -0
- Demo/五子棋.html +525 -0
- Demo/大鱼吃小鱼.gif +3 -0
- Demo/大鱼吃小鱼.html +562 -0
- README.md +284 -0
- TeleCoder3_vllm_tool_parser/.gitignore +2 -0
- TeleCoder3_vllm_tool_parser/README.md +12 -0
- TeleCoder3_vllm_tool_parser/setup.py +15 -0
- TeleCoder3_vllm_tool_parser/telechat3_reasoning.py +32 -0
- TeleCoder3_vllm_tool_parser/telechat3_tool_parser.py +175 -0
- images/TeleCoder3_post_training.png +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
Demo/大鱼吃小鱼.gif filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
Demo/五子棋.gif filter=lfs diff=lfs merge=lfs -text
|
Demo/五子棋.gif
ADDED
|
Git LFS Details
|
Demo/五子棋.html
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>五子棋 - 网页版</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--board-bg: #e3c086;
|
| 10 |
+
--board-line: #5d4037;
|
| 11 |
+
--p1-color: #111; /* 黑棋 */
|
| 12 |
+
--p2-color: #f0f0f0; /* 白棋 */
|
| 13 |
+
--highlight: #ff4757; /* 最后一手的标记颜色 */
|
| 14 |
+
--primary-btn: #2ed573;
|
| 15 |
+
--primary-btn-hover: #26af61;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
body {
|
| 19 |
+
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
| 20 |
+
background-color: #f7f1e3;
|
| 21 |
+
display: flex;
|
| 22 |
+
flex-direction: column;
|
| 23 |
+
align-items: center;
|
| 24 |
+
justify-content: center;
|
| 25 |
+
min-height: 100vh;
|
| 26 |
+
margin: 0;
|
| 27 |
+
user-select: none;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
header {
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
text-align: center;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
h1 {
|
| 36 |
+
color: #2f3542;
|
| 37 |
+
margin: 0;
|
| 38 |
+
font-size: 2rem;
|
| 39 |
+
letter-spacing: 2px;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.game-wrapper {
|
| 43 |
+
display: flex;
|
| 44 |
+
gap: 30px;
|
| 45 |
+
flex-wrap: wrap;
|
| 46 |
+
justify-content: center;
|
| 47 |
+
align-items: flex-start;
|
| 48 |
+
padding: 20px;
|
| 49 |
+
background: white;
|
| 50 |
+
border-radius: 12px;
|
| 51 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* 棋盘区域 */
|
| 55 |
+
.board-container {
|
| 56 |
+
position: relative;
|
| 57 |
+
padding: 15px;
|
| 58 |
+
background-color: var(--board-bg);
|
| 59 |
+
border-radius: 4px;
|
| 60 |
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.3), 5px 5px 15px rgba(0,0,0,0.2);
|
| 61 |
+
/* 棋盘纹理效果 */
|
| 62 |
+
background-image: linear-gradient(45deg, rgba(0,0,0,0.03) 25%, transparent 25%, transparent 75%, rgba(0,0,0,0.03) 75%, rgba(0,0,0,0.03)),
|
| 63 |
+
linear-gradient(45deg, rgba(0,0,0,0.03) 25%, transparent 25%, transparent 75%, rgba(0,0,0,0.03) 75%, rgba(0,0,0,0.03));
|
| 64 |
+
background-size: 20px 20px;
|
| 65 |
+
background-position: 0 0, 10px 10px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.board {
|
| 69 |
+
display: grid;
|
| 70 |
+
grid-template-columns: repeat(15, 30px);
|
| 71 |
+
grid-template-rows: repeat(15, 30px);
|
| 72 |
+
gap: 0;
|
| 73 |
+
position: relative;
|
| 74 |
+
cursor: pointer;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* 棋盘格子与线条 */
|
| 78 |
+
.cell {
|
| 79 |
+
width: 30px;
|
| 80 |
+
height: 30px;
|
| 81 |
+
position: relative;
|
| 82 |
+
display: flex;
|
| 83 |
+
justify-content: center;
|
| 84 |
+
align-items: center;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* 绘制棋盘线:利用伪元素模拟交叉点 */
|
| 88 |
+
.cell::before {
|
| 89 |
+
content: '';
|
| 90 |
+
position: absolute;
|
| 91 |
+
top: 50%;
|
| 92 |
+
left: 0;
|
| 93 |
+
width: 100%;
|
| 94 |
+
height: 1px;
|
| 95 |
+
background-color: var(--board-line);
|
| 96 |
+
z-index: 0;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.cell::after {
|
| 100 |
+
content: '';
|
| 101 |
+
position: absolute;
|
| 102 |
+
left: 50%;
|
| 103 |
+
top: 0;
|
| 104 |
+
height: 100%;
|
| 105 |
+
width: 1px;
|
| 106 |
+
background-color: var(--board-line);
|
| 107 |
+
z-index: 0;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* 天元和星位小黑点 (可选,仅作装饰) */
|
| 111 |
+
.cell.star-point::before {
|
| 112 |
+
width: 100%; /* 保持线条贯穿 */
|
| 113 |
+
}
|
| 114 |
+
.cell.star-point .dot {
|
| 115 |
+
position: absolute;
|
| 116 |
+
width: 6px;
|
| 117 |
+
height: 6px;
|
| 118 |
+
background: var(--board-line);
|
| 119 |
+
border-radius: 50%;
|
| 120 |
+
z-index: 1;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* 棋子通用样式 */
|
| 124 |
+
.piece {
|
| 125 |
+
width: 24px;
|
| 126 |
+
height: 24px;
|
| 127 |
+
border-radius: 50%;
|
| 128 |
+
z-index: 2;
|
| 129 |
+
transform: scale(0);
|
| 130 |
+
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 131 |
+
box-shadow: 2px 2px 4px rgba(0,0,0,0.4);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.piece.show {
|
| 135 |
+
transform: scale(1);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/* 玩家1 - 黑棋 */
|
| 139 |
+
.piece.p1 {
|
| 140 |
+
background: radial-gradient(circle at 30% 30%, #555, #000);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* 玩家2 - 白棋 */
|
| 144 |
+
.piece.p2 {
|
| 145 |
+
background: radial-gradient(circle at 30% 30%, #fff, #ddd);
|
| 146 |
+
border: 1px solid #ccc;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/* 最新落子标记 */
|
| 150 |
+
.piece.last-move::after {
|
| 151 |
+
content: '';
|
| 152 |
+
position: absolute;
|
| 153 |
+
top: 50%;
|
| 154 |
+
left: 50%;
|
| 155 |
+
transform: translate(-50%, -50%);
|
| 156 |
+
width: 6px;
|
| 157 |
+
height: 6px;
|
| 158 |
+
background-color: var(--highlight);
|
| 159 |
+
border-radius: 50%;
|
| 160 |
+
box-shadow: 0 0 4px rgba(255, 71, 87, 0.8);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/* 鼠标悬停预览 */
|
| 164 |
+
.cell:hover:not(.occupied) .preview {
|
| 165 |
+
display: block;
|
| 166 |
+
}
|
| 167 |
+
.preview {
|
| 168 |
+
display: none;
|
| 169 |
+
width: 24px;
|
| 170 |
+
height: 24px;
|
| 171 |
+
border-radius: 50%;
|
| 172 |
+
opacity: 0.4;
|
| 173 |
+
z-index: 1;
|
| 174 |
+
}
|
| 175 |
+
.preview.p1 { background-color: #000; }
|
| 176 |
+
.preview.p2 { background-color: #fff; }
|
| 177 |
+
|
| 178 |
+
/* 侧边栏控制区 */
|
| 179 |
+
.sidebar {
|
| 180 |
+
width: 200px;
|
| 181 |
+
display: flex;
|
| 182 |
+
flex-direction: column;
|
| 183 |
+
align-items: center;
|
| 184 |
+
gap: 20px;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.status-card {
|
| 188 |
+
background: #f1f2f6;
|
| 189 |
+
padding: 20px;
|
| 190 |
+
border-radius: 8px;
|
| 191 |
+
width: 100%;
|
| 192 |
+
text-align: center;
|
| 193 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.status-title {
|
| 197 |
+
font-size: 0.9rem;
|
| 198 |
+
color: #747d8c;
|
| 199 |
+
margin-bottom: 10px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.current-player-indicator {
|
| 203 |
+
display: flex;
|
| 204 |
+
align-items: center;
|
| 205 |
+
justify-content: center;
|
| 206 |
+
gap: 10px;
|
| 207 |
+
font-size: 1.2rem;
|
| 208 |
+
font-weight: bold;
|
| 209 |
+
margin-bottom: 15px;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.indicator-dot {
|
| 213 |
+
width: 20px;
|
| 214 |
+
height: 20px;
|
| 215 |
+
border-radius: 50%;
|
| 216 |
+
border: 1px solid #ccc;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.message-area {
|
| 220 |
+
min-height: 40px;
|
| 221 |
+
display: flex;
|
| 222 |
+
align-items: center;
|
| 223 |
+
justify-content: center;
|
| 224 |
+
font-weight: bold;
|
| 225 |
+
color: #2f3542;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.message-area.win { color: var(--primary-btn); }
|
| 229 |
+
.message-area.draw { color: #ffa502; }
|
| 230 |
+
|
| 231 |
+
button {
|
| 232 |
+
background-color: var(--primary-btn);
|
| 233 |
+
color: white;
|
| 234 |
+
border: none;
|
| 235 |
+
padding: 12px 24px;
|
| 236 |
+
font-size: 1rem;
|
| 237 |
+
border-radius: 6px;
|
| 238 |
+
cursor: pointer;
|
| 239 |
+
transition: background 0.2s, transform 0.1s;
|
| 240 |
+
width: 100%;
|
| 241 |
+
font-weight: bold;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
button:hover {
|
| 245 |
+
background-color: var(--primary-btn-hover);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
button:active {
|
| 249 |
+
transform: scale(0.98);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/* 响应式调整 */
|
| 253 |
+
@media (max-width: 600px) {
|
| 254 |
+
.game-wrapper {
|
| 255 |
+
flex-direction: column;
|
| 256 |
+
align-items: center;
|
| 257 |
+
}
|
| 258 |
+
.sidebar {
|
| 259 |
+
width: 100%;
|
| 260 |
+
flex-direction: row;
|
| 261 |
+
justify-content: space-between;
|
| 262 |
+
box-sizing: border-box;
|
| 263 |
+
padding: 10px;
|
| 264 |
+
}
|
| 265 |
+
.status-card {
|
| 266 |
+
flex: 1;
|
| 267 |
+
margin-right: 10px;
|
| 268 |
+
}
|
| 269 |
+
button {
|
| 270 |
+
width: auto;
|
| 271 |
+
padding: 10px 20px;
|
| 272 |
+
}
|
| 273 |
+
.board {
|
| 274 |
+
/* 移动端稍微缩小棋盘 */
|
| 275 |
+
grid-template-columns: repeat(15, 22px);
|
| 276 |
+
grid-template-rows: repeat(15, 22px);
|
| 277 |
+
}
|
| 278 |
+
.cell {
|
| 279 |
+
width: 22px;
|
| 280 |
+
height: 22px;
|
| 281 |
+
}
|
| 282 |
+
.piece {
|
| 283 |
+
width: 18px;
|
| 284 |
+
height: 18px;
|
| 285 |
+
}
|
| 286 |
+
.preview {
|
| 287 |
+
width: 18px;
|
| 288 |
+
height: 18px;
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
</style>
|
| 292 |
+
</head>
|
| 293 |
+
<body>
|
| 294 |
+
|
| 295 |
+
<header>
|
| 296 |
+
<h1>五子棋</h1>
|
| 297 |
+
</header>
|
| 298 |
+
|
| 299 |
+
<main class="game-wrapper">
|
| 300 |
+
<!-- 棋盘区域 -->
|
| 301 |
+
<div class="board-container">
|
| 302 |
+
<div class="board" id="board"></div>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<!-- 控制与状态区域 -->
|
| 306 |
+
<aside class="sidebar">
|
| 307 |
+
<div class="status-card">
|
| 308 |
+
<div class="status-title">游戏状态</div>
|
| 309 |
+
<div class="current-player-indicator">
|
| 310 |
+
<span id="playerDot" class="indicator-dot" style="background: #000;"></span>
|
| 311 |
+
<span id="statusText">游戏开始!玩家1先落子</span>
|
| 312 |
+
</div>
|
| 313 |
+
<div class="message-area" id="messageArea">
|
| 314 |
+
等待落子...
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
<button id="restartBtn" onclick="game.restart()">重新开始</button>
|
| 318 |
+
</aside>
|
| 319 |
+
</main>
|
| 320 |
+
|
| 321 |
+
<script>
|
| 322 |
+
/**
|
| 323 |
+
* 游戏逻辑控制器
|
| 324 |
+
*/
|
| 325 |
+
const game = {
|
| 326 |
+
size: 15, // 15x15 棋盘
|
| 327 |
+
boardData: [], // 存储棋盘状态: 0=空, 1=玩家1(黑), 2=玩家2(白)
|
| 328 |
+
currentPlayer: 1, // 1 或 2
|
| 329 |
+
isGameOver: false,
|
| 330 |
+
moveCount: 0,
|
| 331 |
+
maxMoves: 15 * 15,
|
| 332 |
+
lastMove: null, // {r, c} 记录最后一步,用于高亮
|
| 333 |
+
|
| 334 |
+
// DOM 元素引用
|
| 335 |
+
boardEl: document.getElementById('board'),
|
| 336 |
+
statusTextEl: document.getElementById('statusText'),
|
| 337 |
+
messageAreaEl: document.getElementById('messageArea'),
|
| 338 |
+
playerDotEl: document.getElementById('playerDot'),
|
| 339 |
+
restartBtn: document.getElementById('restartBtn'),
|
| 340 |
+
|
| 341 |
+
// 初始化游戏
|
| 342 |
+
init() {
|
| 343 |
+
this.createBoard();
|
| 344 |
+
this.restart();
|
| 345 |
+
},
|
| 346 |
+
|
| 347 |
+
// 创建棋盘网格 HTML
|
| 348 |
+
createBoard() {
|
| 349 |
+
this.boardEl.innerHTML = '';
|
| 350 |
+
for (let r = 0; r < this.size; r++) {
|
| 351 |
+
for (let c = 0; c < this.size; c++) {
|
| 352 |
+
const cell = document.createElement('div');
|
| 353 |
+
cell.classList.add('cell');
|
| 354 |
+
cell.dataset.row = r;
|
| 355 |
+
cell.dataset.col = c;
|
| 356 |
+
|
| 357 |
+
// 添加星位 (标准五子棋星位位置: 3,3; 11,3; 7,7; 3,11; 11,11)
|
| 358 |
+
if ((r === 3 || r === 11 || r === 7) && (c === 3 || c === 11 || c === 7)) {
|
| 359 |
+
const dot = document.createElement('div');
|
| 360 |
+
dot.classList.add('dot');
|
| 361 |
+
cell.classList.add('star-point');
|
| 362 |
+
cell.appendChild(dot);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
// 添加预览棋子 (根据当前回合)
|
| 366 |
+
const preview = document.createElement('div');
|
| 367 |
+
preview.classList.add('preview', `p${this.currentPlayer}`);
|
| 368 |
+
cell.appendChild(preview);
|
| 369 |
+
|
| 370 |
+
// 绑定点击事件
|
| 371 |
+
cell.addEventListener('click', () => this.handleMove(r, c));
|
| 372 |
+
this.boardEl.appendChild(cell);
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
},
|
| 376 |
+
|
| 377 |
+
// 重新开始
|
| 378 |
+
restart() {
|
| 379 |
+
this.boardData = Array(this.size).fill(null).map(() => Array(this.size).fill(0));
|
| 380 |
+
this.currentPlayer = 1;
|
| 381 |
+
this.isGameOver = false;
|
| 382 |
+
this.moveCount = 0;
|
| 383 |
+
this.lastMove = null;
|
| 384 |
+
|
| 385 |
+
// 清除所有棋子和状态
|
| 386 |
+
const cells = document.querySelectorAll('.cell');
|
| 387 |
+
cells.forEach(cell => {
|
| 388 |
+
// 移除 .occupied 类(虽然这里没用这个类做太多,但逻辑上表示被占用了)
|
| 389 |
+
cell.classList.remove('occupied');
|
| 390 |
+
const piece = cell.querySelector('.piece');
|
| 391 |
+
if (piece) piece.remove();
|
| 392 |
+
|
| 393 |
+
// 重置预览
|
| 394 |
+
const preview = cell.querySelector('.preview');
|
| 395 |
+
if(preview) {
|
| 396 |
+
preview.className = `preview p${this.currentPlayer}`;
|
| 397 |
+
}
|
| 398 |
+
});
|
| 399 |
+
|
| 400 |
+
this.updateUI();
|
| 401 |
+
this.messageAreaEl.textContent = "游戏开始!玩家1先落子";
|
| 402 |
+
this.messageAreaEl.className = "message-area";
|
| 403 |
+
this.statusTextEl.textContent = "轮到玩家1";
|
| 404 |
+
},
|
| 405 |
+
|
| 406 |
+
// 处理落子
|
| 407 |
+
handleMove(r, c) {
|
| 408 |
+
// 如果游戏结束或该位置已有棋子,则忽略
|
| 409 |
+
if (this.isGameOver || this.boardData[r][c] !== 0) return;
|
| 410 |
+
|
| 411 |
+
// 1. 更新数据
|
| 412 |
+
this.boardData[r][c] = this.currentPlayer;
|
| 413 |
+
this.moveCount++;
|
| 414 |
+
this.lastMove = { r, c };
|
| 415 |
+
|
| 416 |
+
// 2. 更新 UI:移除预览,添加正式棋子
|
| 417 |
+
const cell = this.getCell(r, c);
|
| 418 |
+
const preview = cell.querySelector('.preview');
|
| 419 |
+
if (preview) preview.style.display = 'none';
|
| 420 |
+
|
| 421 |
+
const piece = document.createElement('div');
|
| 422 |
+
piece.classList.add('piece', `p${this.currentPlayer}`, 'show');
|
| 423 |
+
|
| 424 |
+
// 移除旧的高亮,添加新的高亮
|
| 425 |
+
const prevLast = document.querySelector('.piece.last-move');
|
| 426 |
+
if (prevLast) prevLast.classList.remove('last-move');
|
| 427 |
+
piece.classList.add('last-move');
|
| 428 |
+
|
| 429 |
+
cell.appendChild(piece);
|
| 430 |
+
|
| 431 |
+
// 3. 检查胜负
|
| 432 |
+
if (this.checkWin(r, c, this.currentPlayer)) {
|
| 433 |
+
this.endGame(true);
|
| 434 |
+
return;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
// 4. 检查平局
|
| 438 |
+
if (this.moveCount === this.maxMoves) {
|
| 439 |
+
this.endGame(false);
|
| 440 |
+
return;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
// 5. 切换玩家
|
| 444 |
+
this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
|
| 445 |
+
|
| 446 |
+
// 更新所有格子的悬停预览颜色(因为换了玩家)
|
| 447 |
+
this.updatePreviews();
|
| 448 |
+
this.updateUI();
|
| 449 |
+
},
|
| 450 |
+
|
| 451 |
+
// 更新界面文字和指示器
|
| 452 |
+
updateUI() {
|
| 453 |
+
this.statusTextEl.textContent = `轮到玩家${this.currentPlayer}`;
|
| 454 |
+
this.playerDotEl.style.background = this.currentPlayer === 1 ? '#000' : '#fff';
|
| 455 |
+
this.playerDotEl.style.border = this.currentPlayer === 1 ? 'none' : '1px solid #ccc';
|
| 456 |
+
},
|
| 457 |
+
|
| 458 |
+
// 更新棋盘上的悬停预览颜色
|
| 459 |
+
updatePreviews() {
|
| 460 |
+
const previews = document.querySelectorAll('.preview');
|
| 461 |
+
previews.forEach(p => {
|
| 462 |
+
p.className = `preview p${this.currentPlayer}`;
|
| 463 |
+
});
|
| 464 |
+
},
|
| 465 |
+
|
| 466 |
+
// 获取 DOM 单元格
|
| 467 |
+
getCell(r, c) {
|
| 468 |
+
// 由于 grid 是平铺的,索引 = r * size + c
|
| 469 |
+
return this.boardEl.children[r * this.size + c];
|
| 470 |
+
},
|
| 471 |
+
|
| 472 |
+
// 核心算法:检查胜利
|
| 473 |
+
checkWin(r, c, player) {
|
| 474 |
+
const directions = [
|
| 475 |
+
[[0, 1], [0, -1]], // 水平
|
| 476 |
+
[[1, 0], [-1, 0]], // 垂直
|
| 477 |
+
[[1, 1], [-1, -1]], // 右下斜
|
| 478 |
+
[[1, -1], [-1, 1]] // 左下斜
|
| 479 |
+
];
|
| 480 |
+
|
| 481 |
+
for (let axis of directions) {
|
| 482 |
+
let count = 1; // 包含当前落子
|
| 483 |
+
|
| 484 |
+
for (let dir of axis) {
|
| 485 |
+
let nr = r + dir[0];
|
| 486 |
+
let nc = c + dir[1];
|
| 487 |
+
|
| 488 |
+
while (
|
| 489 |
+
nr >= 0 && nr < this.size &&
|
| 490 |
+
nc >= 0 && nc < this.size &&
|
| 491 |
+
this.boardData[nr][nc] === player
|
| 492 |
+
) {
|
| 493 |
+
count++;
|
| 494 |
+
nr += dir[0];
|
| 495 |
+
nc += dir[1];
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
if (count >= 5) return true;
|
| 500 |
+
}
|
| 501 |
+
return false;
|
| 502 |
+
},
|
| 503 |
+
|
| 504 |
+
// 游戏结束处理
|
| 505 |
+
endGame(hasWinner) {
|
| 506 |
+
this.isGameOver = true;
|
| 507 |
+
if (hasWinner) {
|
| 508 |
+
const winnerName = this.currentPlayer === 1 ? "玩家1 (黑)" : "玩家2 (白)";
|
| 509 |
+
this.messageAreaEl.textContent = `${winnerName} 获胜!`;
|
| 510 |
+
this.messageAreaEl.className = "message-area win";
|
| 511 |
+
this.statusTextEl.textContent = "游戏结束";
|
| 512 |
+
} else {
|
| 513 |
+
this.messageAreaEl.textContent = "平局!";
|
| 514 |
+
this.messageAreaEl.className = "message-area draw";
|
| 515 |
+
this.statusTextEl.textContent = "游戏结束";
|
| 516 |
+
}
|
| 517 |
+
}
|
| 518 |
+
};
|
| 519 |
+
|
| 520 |
+
// 启动游戏
|
| 521 |
+
game.init();
|
| 522 |
+
|
| 523 |
+
</script>
|
| 524 |
+
</body>
|
| 525 |
+
</html>
|
Demo/大鱼吃小鱼.gif
ADDED
|
Git LFS Details
|
Demo/大鱼吃小鱼.html
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>大鱼吃小鱼</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--water-top: #006994;
|
| 10 |
+
--water-bottom: #001e36;
|
| 11 |
+
--ui-bg: rgba(255, 255, 255, 0.9);
|
| 12 |
+
--accent-color: #ff9800;
|
| 13 |
+
--text-color: #333;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
body {
|
| 17 |
+
margin: 0;
|
| 18 |
+
padding: 0;
|
| 19 |
+
display: flex;
|
| 20 |
+
justify-content: center;
|
| 21 |
+
align-items: center;
|
| 22 |
+
height: 100vh;
|
| 23 |
+
background-color: #222;
|
| 24 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 25 |
+
overflow: hidden;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* 游戏主容器 */
|
| 29 |
+
#game-wrapper {
|
| 30 |
+
position: relative;
|
| 31 |
+
width: 800px;
|
| 32 |
+
height: 600px;
|
| 33 |
+
box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
|
| 34 |
+
border-radius: 12px;
|
| 35 |
+
overflow: hidden;
|
| 36 |
+
background: linear-gradient(to bottom, var(--water-top), var(--water-bottom));
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
canvas {
|
| 40 |
+
display: block;
|
| 41 |
+
width: 100%;
|
| 42 |
+
height: 100%;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* UI 面板 */
|
| 46 |
+
#ui-panel {
|
| 47 |
+
position: absolute;
|
| 48 |
+
top: 0;
|
| 49 |
+
left: 0;
|
| 50 |
+
width: 100%;
|
| 51 |
+
padding: 15px 20px;
|
| 52 |
+
box-sizing: border-box;
|
| 53 |
+
display: flex;
|
| 54 |
+
justify-content: space-between;
|
| 55 |
+
align-items: center;
|
| 56 |
+
pointer-events: none; /* 让点击穿透到Canvas,除非是按钮 */
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.score-board {
|
| 60 |
+
background: rgba(0, 0, 0, 0.4);
|
| 61 |
+
color: white;
|
| 62 |
+
padding: 8px 16px;
|
| 63 |
+
border-radius: 20px;
|
| 64 |
+
font-size: 18px;
|
| 65 |
+
font-weight: bold;
|
| 66 |
+
pointer-events: auto;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.controls-hint {
|
| 70 |
+
color: rgba(255, 255, 255, 0.7);
|
| 71 |
+
font-size: 14px;
|
| 72 |
+
pointer-events: auto;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* 状态消息区 */
|
| 76 |
+
#status-message {
|
| 77 |
+
position: absolute;
|
| 78 |
+
bottom: 20px;
|
| 79 |
+
left: 50%;
|
| 80 |
+
transform: translateX(-50%);
|
| 81 |
+
background: rgba(0, 0, 0, 0.5);
|
| 82 |
+
color: #fff;
|
| 83 |
+
padding: 8px 20px;
|
| 84 |
+
border-radius: 4px;
|
| 85 |
+
font-size: 14px;
|
| 86 |
+
opacity: 0;
|
| 87 |
+
transition: opacity 0.3s;
|
| 88 |
+
pointer-events: none;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/* 模态框 (开始/结束/胜利) */
|
| 92 |
+
#modal-overlay {
|
| 93 |
+
position: absolute;
|
| 94 |
+
top: 0;
|
| 95 |
+
left: 0;
|
| 96 |
+
width: 100%;
|
| 97 |
+
height: 100%;
|
| 98 |
+
background: rgba(0, 0, 0, 0.6);
|
| 99 |
+
backdrop-filter: blur(4px);
|
| 100 |
+
display: flex;
|
| 101 |
+
justify-content: center;
|
| 102 |
+
align-items: center;
|
| 103 |
+
z-index: 10;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.modal-content {
|
| 107 |
+
background: white;
|
| 108 |
+
padding: 40px;
|
| 109 |
+
border-radius: 16px;
|
| 110 |
+
text-align: center;
|
| 111 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 112 |
+
max-width: 400px;
|
| 113 |
+
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
@keyframes popIn {
|
| 117 |
+
from { transform: scale(0.8); opacity: 0; }
|
| 118 |
+
to { transform: scale(1); opacity: 1; }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
h1 { margin: 0 0 10px; color: var(--water-top); }
|
| 122 |
+
h2 { margin: 0 0 20px; color: var(--text-color); }
|
| 123 |
+
p { color: #666; margin-bottom: 30px; line-height: 1.5; }
|
| 124 |
+
|
| 125 |
+
.btn {
|
| 126 |
+
background-color: var(--accent-color);
|
| 127 |
+
color: white;
|
| 128 |
+
border: none;
|
| 129 |
+
padding: 12px 30px;
|
| 130 |
+
font-size: 18px;
|
| 131 |
+
border-radius: 30px;
|
| 132 |
+
cursor: pointer;
|
| 133 |
+
transition: transform 0.1s, background-color 0.2s;
|
| 134 |
+
font-weight: bold;
|
| 135 |
+
outline: none;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.btn:hover { background-color: #e68900; transform: scale(1.05); }
|
| 139 |
+
.btn:active { transform: scale(0.95); }
|
| 140 |
+
|
| 141 |
+
.hidden { display: none !important; }
|
| 142 |
+
|
| 143 |
+
</style>
|
| 144 |
+
</head>
|
| 145 |
+
<body>
|
| 146 |
+
|
| 147 |
+
<div id="game-wrapper">
|
| 148 |
+
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
| 149 |
+
|
| 150 |
+
<!-- 顶部UI -->
|
| 151 |
+
<div id="ui-panel">
|
| 152 |
+
<div class="score-board">得分: <span id="score-display">0</span></div>
|
| 153 |
+
<div class="controls-hint">使用方向键 ↑ ↓ ← → 控制移动</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<!-- 底部消息反馈 -->
|
| 157 |
+
<div id="status-message">游戏开始!</div>
|
| 158 |
+
|
| 159 |
+
<!-- 游戏状态弹窗 -->
|
| 160 |
+
<div id="modal-overlay">
|
| 161 |
+
<div class="modal-content" id="start-screen">
|
| 162 |
+
<h1>大鱼吃小鱼</h1>
|
| 163 |
+
<p>
|
| 164 |
+
吃掉比你小的鱼来成长。<br>
|
| 165 |
+
躲避比你大的鱼!<br>
|
| 166 |
+
目标:达到最大的体型(半径50)。
|
| 167 |
+
</p>
|
| 168 |
+
<button class="btn" onclick="game.start()">开始游戏</button>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<div class="modal-content hidden" id="game-over-screen">
|
| 172 |
+
<h2 style="color: #d32f2f;">被大鱼吃掉了!</h2>
|
| 173 |
+
<p>最终得分: <span id="final-score-loss">0</span></p>
|
| 174 |
+
<button class="btn" onclick="game.restart()">重新开始</button>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<div class="modal-content hidden" id="victory-screen">
|
| 178 |
+
<h2 style="color: #388e3c;">成为最大的鱼!</h2>
|
| 179 |
+
<p>恭喜你通关成功!<br>你已经是海洋霸主了。</p>
|
| 180 |
+
<p>最终得分: <span id="final-score-win">0</span></p>
|
| 181 |
+
<button class="btn" onclick="game.restart()">再玩一次</button>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<script>
|
| 187 |
+
/**
|
| 188 |
+
* 游戏配置与常量
|
| 189 |
+
*/
|
| 190 |
+
const CONFIG = {
|
| 191 |
+
friction: 0.95, // 移动摩擦力
|
| 192 |
+
playerBaseSpeed: 0.5,
|
| 193 |
+
playerMaxSpeed: 6,
|
| 194 |
+
enemyBaseSpeed: 2,
|
| 195 |
+
winRadius: 50, // 胜利半径
|
| 196 |
+
spawnRate: 60, // 每多少帧尝试生成敌人 (越小越快)
|
| 197 |
+
colors: {
|
| 198 |
+
player: '#FF9800',
|
| 199 |
+
small: '#4CAF50', // 绿色:可吃
|
| 200 |
+
big: '#F44336' // 红色:危险
|
| 201 |
+
}
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
/**
|
| 205 |
+
* 鱼类基类
|
| 206 |
+
*/
|
| 207 |
+
class Fish {
|
| 208 |
+
constructor(x, y, radius, color, isPlayer = false) {
|
| 209 |
+
this.x = x;
|
| 210 |
+
this.y = y;
|
| 211 |
+
this.radius = radius;
|
| 212 |
+
this.color = color;
|
| 213 |
+
this.isPlayer = isPlayer;
|
| 214 |
+
this.angle = 0; // 面向角度
|
| 215 |
+
|
| 216 |
+
// 速度向量
|
| 217 |
+
this.velocity = { x: 0, y: 0 };
|
| 218 |
+
|
| 219 |
+
if (isPlayer) {
|
| 220 |
+
this.speed = CONFIG.playerBaseSpeed;
|
| 221 |
+
} else {
|
| 222 |
+
this.speed = Math.random() * 1.5 + 0.5; // 随机速度
|
| 223 |
+
// 敌人随机方向
|
| 224 |
+
this.angle = Math.random() * Math.PI * 2;
|
| 225 |
+
this.updateVelocity();
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
updateVelocity() {
|
| 230 |
+
// 根据当前角度设置速度向量
|
| 231 |
+
this.velocity.x = Math.cos(this.angle) * this.speed;
|
| 232 |
+
this.velocity.y = Math.sin(this.angle) * this.speed;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
move() {
|
| 236 |
+
this.x += this.velocity.x;
|
| 237 |
+
this.y += this.velocity.y;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// 边界反弹
|
| 241 |
+
checkBounds(width, height) {
|
| 242 |
+
if (this.x - this.radius < 0) {
|
| 243 |
+
this.x = this.radius;
|
| 244 |
+
this.angle = Math.PI - this.angle;
|
| 245 |
+
this.updateVelocity();
|
| 246 |
+
}
|
| 247 |
+
if (this.x + this.radius > width) {
|
| 248 |
+
this.x = width - this.radius;
|
| 249 |
+
this.angle = Math.PI - this.angle;
|
| 250 |
+
this.updateVelocity();
|
| 251 |
+
}
|
| 252 |
+
if (this.y - this.radius < 0) {
|
| 253 |
+
this.y = this.radius;
|
| 254 |
+
this.angle = -this.angle;
|
| 255 |
+
this.updateVelocity();
|
| 256 |
+
}
|
| 257 |
+
if (this.y + this.radius > height) {
|
| 258 |
+
this.y = height - this.radius;
|
| 259 |
+
this.angle = -this.angle;
|
| 260 |
+
this.updateVelocity();
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
draw(ctx) {
|
| 265 |
+
ctx.save();
|
| 266 |
+
ctx.translate(this.x, this.y);
|
| 267 |
+
ctx.rotate(this.angle);
|
| 268 |
+
|
| 269 |
+
// 身体
|
| 270 |
+
ctx.beginPath();
|
| 271 |
+
ctx.fillStyle = this.color;
|
| 272 |
+
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
|
| 273 |
+
ctx.fill();
|
| 274 |
+
|
| 275 |
+
// 眼睛
|
| 276 |
+
ctx.beginPath();
|
| 277 |
+
ctx.fillStyle = 'white';
|
| 278 |
+
ctx.arc(this.radius * 0.4, -this.radius * 0.3, this.radius * 0.25, 0, Math.PI * 2);
|
| 279 |
+
ctx.fill();
|
| 280 |
+
ctx.beginPath();
|
| 281 |
+
ctx.fillStyle = 'black';
|
| 282 |
+
ctx.arc(this.radius * 0.5, -this.radius * 0.3, this.radius * 0.1, 0, Math.PI * 2);
|
| 283 |
+
ctx.fill();
|
| 284 |
+
|
| 285 |
+
// 尾巴
|
| 286 |
+
ctx.beginPath();
|
| 287 |
+
ctx.fillStyle = this.color;
|
| 288 |
+
ctx.moveTo(-this.radius * 0.8, 0);
|
| 289 |
+
ctx.lineTo(-this.radius * 1.5, -this.radius * 0.6);
|
| 290 |
+
ctx.lineTo(-this.radius * 1.5, this.radius * 0.6);
|
| 291 |
+
ctx.fill();
|
| 292 |
+
|
| 293 |
+
ctx.restore();
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
grow(amount) {
|
| 297 |
+
this.radius += amount;
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/**
|
| 302 |
+
* 游戏主逻辑类
|
| 303 |
+
*/
|
| 304 |
+
class Game {
|
| 305 |
+
constructor() {
|
| 306 |
+
this.canvas = document.getElementById('gameCanvas');
|
| 307 |
+
this.ctx = this.canvas.getContext('2d');
|
| 308 |
+
this.width = this.canvas.width;
|
| 309 |
+
this.height = this.canvas.height;
|
| 310 |
+
|
| 311 |
+
this.score = 0;
|
| 312 |
+
this.state = 'START'; // START, PLAYING, GAME_OVER, VICTORY
|
| 313 |
+
this.frameCount = 0;
|
| 314 |
+
|
| 315 |
+
// 实体
|
| 316 |
+
this.player = null;
|
| 317 |
+
this.enemies = [];
|
| 318 |
+
this.particles = []; // 可以添加吃鱼特效
|
| 319 |
+
|
| 320 |
+
// 输入状态
|
| 321 |
+
this.keys = {
|
| 322 |
+
ArrowUp: false,
|
| 323 |
+
ArrowDown: false,
|
| 324 |
+
ArrowLeft: false,
|
| 325 |
+
ArrowRight: false
|
| 326 |
+
};
|
| 327 |
+
|
| 328 |
+
this.bindEvents();
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
bindEvents() {
|
| 332 |
+
window.addEventListener('keydown', (e) => {
|
| 333 |
+
if (this.keys.hasOwnProperty(e.code)) this.keys[e.code] = true;
|
| 334 |
+
});
|
| 335 |
+
window.addEventListener('keyup', (e) => {
|
| 336 |
+
if (this.keys.hasOwnProperty(e.code)) this.keys[e.code] = false;
|
| 337 |
+
});
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
init() {
|
| 341 |
+
// 初始化玩家
|
| 342 |
+
this.player = new Fish(this.width / 2, this.height / 2, 10, CONFIG.colors.player, true);
|
| 343 |
+
this.enemies = [];
|
| 344 |
+
this.score = 0;
|
| 345 |
+
this.frameCount = 0;
|
| 346 |
+
this.updateUI();
|
| 347 |
+
this.showMessage("准备开始...");
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
start() {
|
| 351 |
+
this.init();
|
| 352 |
+
this.state = 'PLAYING';
|
| 353 |
+
this.hideModals();
|
| 354 |
+
this.animate();
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
restart() {
|
| 358 |
+
this.start();
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
spawnEnemy() {
|
| 362 |
+
// 在边缘生成敌人
|
| 363 |
+
let x, y;
|
| 364 |
+
if (Math.random() < 0.5) {
|
| 365 |
+
x = Math.random() < 0.5 ? -30 : this.width + 30;
|
| 366 |
+
y = Math.random() * this.height;
|
| 367 |
+
} else {
|
| 368 |
+
x = Math.random() * this.width;
|
| 369 |
+
y = Math.random() < 0.5 ? -30 : this.height + 30;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// 随机大小:有的比玩家小,有的比玩家大
|
| 373 |
+
// 动态调整:确保大概50%概率比玩家小,50%比玩家大,但差距适中
|
| 374 |
+
let sizeVariation = Math.random();
|
| 375 |
+
let radius;
|
| 376 |
+
if (sizeVariation > 0.5) {
|
| 377 |
+
// 生成比玩家小的 (食物)
|
| 378 |
+
radius = Math.max(5, this.player.radius * (0.5 + Math.random() * 0.4));
|
| 379 |
+
} else {
|
| 380 |
+
// 生成比玩家大的 (威胁)
|
| 381 |
+
radius = this.player.radius * (1.2 + Math.random() * 1.5);
|
| 382 |
+
// 限制最大尺寸,防止生成无敌怪
|
| 383 |
+
if (radius > 100) radius = 100;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// 颜色逻辑
|
| 387 |
+
let color = CONFIG.colors.small;
|
| 388 |
+
if (radius > this.player.radius) color = CONFIG.colors.big;
|
| 389 |
+
|
| 390 |
+
const enemy = new Fish(x, y, radius, color);
|
| 391 |
+
|
| 392 |
+
// 让敌人朝向玩家大概方向移动,增加互动感
|
| 393 |
+
const angleToPlayer = Math.atan2(this.player.y - y, this.player.x - x);
|
| 394 |
+
// 增加一些随机偏移,不要死板地冲向玩家
|
| 395 |
+
const variance = (Math.random() - 0.5);
|
| 396 |
+
enemy.angle = angleToPlayer + variance;
|
| 397 |
+
enemy.updateVelocity();
|
| 398 |
+
|
| 399 |
+
this.enemies.push(enemy);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
handleInput() {
|
| 403 |
+
if (this.state !== 'PLAYING') return;
|
| 404 |
+
|
| 405 |
+
// 玩家控制逻辑
|
| 406 |
+
if (this.keys.ArrowUp) {
|
| 407 |
+
this.player.velocity.y -= CONFIG.playerBaseSpeed;
|
| 408 |
+
this.player.angle = -Math.PI / 2;
|
| 409 |
+
}
|
| 410 |
+
if (this.keys.ArrowDown) {
|
| 411 |
+
this.player.velocity.y += CONFIG.playerBaseSpeed;
|
| 412 |
+
this.player.angle = Math.PI / 2;
|
| 413 |
+
}
|
| 414 |
+
if (this.keys.ArrowLeft) {
|
| 415 |
+
this.player.velocity.x -= CONFIG.playerBaseSpeed;
|
| 416 |
+
this.player.angle = Math.PI;
|
| 417 |
+
}
|
| 418 |
+
if (this.keys.ArrowRight) {
|
| 419 |
+
this.player.velocity.x += CONFIG.playerBaseSpeed;
|
| 420 |
+
this.player.angle = 0;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// 限制最大速度
|
| 424 |
+
const speed = Math.sqrt(this.player.velocity.x**2 + this.player.velocity.y**2);
|
| 425 |
+
if (speed > CONFIG.playerMaxSpeed) {
|
| 426 |
+
const ratio = CONFIG.playerMaxSpeed / speed;
|
| 427 |
+
this.player.velocity.x *= ratio;
|
| 428 |
+
this.player.velocity.y *= ratio;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
// 应用摩擦力
|
| 432 |
+
this.player.velocity.x *= CONFIG.friction;
|
| 433 |
+
this.player.velocity.y *= CONFIG.friction;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
checkCollisions() {
|
| 437 |
+
for (let i = this.enemies.length - 1; i >= 0; i--) {
|
| 438 |
+
const enemy = this.enemies[i];
|
| 439 |
+
const dx = this.player.x - enemy.x;
|
| 440 |
+
const dy = this.player.y - enemy.y;
|
| 441 |
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
| 442 |
+
|
| 443 |
+
// 碰撞发生
|
| 444 |
+
if (distance < this.player.radius + enemy.radius) {
|
| 445 |
+
if (this.player.radius >= enemy.radius) {
|
| 446 |
+
// 玩家吃掉敌人
|
| 447 |
+
this.score += Math.floor(enemy.radius);
|
| 448 |
+
this.player.grow(enemy.radius * 0.05); // 缓慢成长
|
| 449 |
+
|
| 450 |
+
// 视觉反馈
|
| 451 |
+
this.showMessage(`吞噬成功! +${Math.floor(enemy.radius)}分`);
|
| 452 |
+
|
| 453 |
+
// 移除敌人
|
| 454 |
+
this.enemies.splice(i, 1);
|
| 455 |
+
this.updateUI();
|
| 456 |
+
|
| 457 |
+
// 胜利检测
|
| 458 |
+
if (this.player.radius >= CONFIG.winRadius) {
|
| 459 |
+
this.victory();
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
} else {
|
| 463 |
+
// 玩家被吃
|
| 464 |
+
this.gameOver();
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
update() {
|
| 471 |
+
if (this.state !== 'PLAYING') return;
|
| 472 |
+
|
| 473 |
+
this.handleInput();
|
| 474 |
+
|
| 475 |
+
// 移动玩家
|
| 476 |
+
this.player.move();
|
| 477 |
+
this.player.checkBounds(this.width, this.height);
|
| 478 |
+
|
| 479 |
+
// 移动敌人 & 生成
|
| 480 |
+
this.frameCount++;
|
| 481 |
+
if (this.frameCount % CONFIG.spawnRate === 0) {
|
| 482 |
+
this.spawnEnemy();
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
for (let i = this.enemies.length - 1; i >= 0; i--) {
|
| 486 |
+
this.enemies[i].move();
|
| 487 |
+
// 简单的边界检查,防止它们在内部乱飞太远
|
| 488 |
+
// (如果从外部生成,其实不需要,但如果它们被打散就��要)
|
| 489 |
+
this.enemies[i].checkBounds(this.width, this.height);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
this.checkCollisions();
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
draw() {
|
| 496 |
+
// 清空画布
|
| 497 |
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
| 498 |
+
|
| 499 |
+
// 绘制玩家
|
| 500 |
+
if (this.player) this.player.draw(this.ctx);
|
| 501 |
+
|
| 502 |
+
// 绘制敌人
|
| 503 |
+
this.enemies.forEach(enemy => enemy.draw(this.ctx));
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
animate() {
|
| 507 |
+
if (this.state === 'PLAYING') {
|
| 508 |
+
this.update();
|
| 509 |
+
this.draw();
|
| 510 |
+
requestAnimationFrame(() => this.animate());
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
updateUI() {
|
| 515 |
+
document.getElementById('score-display').innerText = this.score;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
showMessage(text) {
|
| 519 |
+
const el = document.getElementById('status-message');
|
| 520 |
+
el.innerText = text;
|
| 521 |
+
el.style.opacity = 1;
|
| 522 |
+
|
| 523 |
+
// 清除之前的定时器(简单处理)
|
| 524 |
+
if (this.msgTimeout) clearTimeout(this.msgTimeout);
|
| 525 |
+
|
| 526 |
+
this.msgTimeout = setTimeout(() => {
|
| 527 |
+
el.style.opacity = 0;
|
| 528 |
+
}, 2000);
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
gameOver() {
|
| 532 |
+
this.state = 'GAME_OVER';
|
| 533 |
+
document.getElementById('final-score-loss').innerText = this.score;
|
| 534 |
+
this.showModal('game-over-screen');
|
| 535 |
+
this.showMessage("被大鱼吃掉了!游戏失败");
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
victory() {
|
| 539 |
+
this.state = 'VICTORY';
|
| 540 |
+
document.getElementById('final-score-win').innerText = this.score;
|
| 541 |
+
this.showModal('victory-screen');
|
| 542 |
+
this.showMessage("成为最大的鱼!通关成功");
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
hideModals() {
|
| 546 |
+
document.querySelectorAll('.modal-content').forEach(el => el.classList.add('hidden'));
|
| 547 |
+
document.getElementById('modal-overlay').classList.add('hidden');
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
showModal(id) {
|
| 551 |
+
this.hideModals();
|
| 552 |
+
document.getElementById(id).classList.remove('hidden');
|
| 553 |
+
document.getElementById('modal-overlay').classList.remove('hidden');
|
| 554 |
+
}
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
// 启动游戏实例
|
| 558 |
+
const game = new Game();
|
| 559 |
+
|
| 560 |
+
</script>
|
| 561 |
+
</body>
|
| 562 |
+
</html>
|
README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<h1>
|
| 3 |
+
TeleCoder3
|
| 4 |
+
</h1>
|
| 5 |
+
</div>
|
| 6 |
+
|
| 7 |
+
<p align="center">
|
| 8 |
+
🦉 <a href="https://github.com/Tele-AI/TeleCoder3" target="_blank">github</a> • 🤗 <a href="https://huggingface.co/Tele-AI" target="_blank">Hugging Face</a> • 🤖 <a href="https://modelscope.cn/organization/TeleAI" target="_blank">ModelScope</a> • 💬 <a href="https://github.com/Tele-AI/Telechat/blob/master/images/wechat.jpg" target="_blank">WeChat</a>
|
| 9 |
+
</p>
|
| 10 |
+
|
| 11 |
+
# 目录
|
| 12 |
+
|
| 13 |
+
- [模型介绍](#模型介绍)
|
| 14 |
+
- [能力评估](#能力评估)
|
| 15 |
+
- [推理](#gpu-推理)
|
| 16 |
+
- [国产化适配](#国产化适配)
|
| 17 |
+
- [声明、协议、引用](#声明协议引用)
|
| 18 |
+
|
| 19 |
+
# 最新动态
|
| 20 |
+
- 2026.01.30 开源 **TeleCoder3-36B-Thinking**
|
| 21 |
+
|
| 22 |
+
# 模型介绍
|
| 23 |
+
|
| 24 |
+
### 星辰语义代码大模型-TeleCoder3
|
| 25 |
+
|
| 26 |
+
- 星辰语义代码大模型 **TeleCoder3** 是由中国电信人工智能研究院研发训练的大语言模型,该模型**完全基于国产算力**训练。
|
| 27 |
+
|
| 28 |
+
### 技术创新
|
| 29 |
+
#### 预训练
|
| 30 |
+
|
| 31 |
+
**TeleCoder3-36B-Thinking** 在 TeleBase3 基础上持续进行三阶段代码预训练:
|
| 32 |
+
- 第一阶段:在 TeleBase3 的基础上,将通用代码数据占比提升至 70%,并基于 8K 上下文长度进行持续训练,以增强模型的代码通识能力;
|
| 33 |
+
- 第二阶段:优化代码数据组成,尤其增加了高质量仓库级别代码长文数据、SWE 级别长程代码任务数据等,以相同数据配比,基于 32K 上下文长度开展持续训练,增加模型长文任务能力;
|
| 34 |
+
- 第三阶段:进一步提升长程代码任务与智能体任务数据占比,并基于 128K 上下文长度进行持续训练,持续增强模型在代码与智能体长程任务场景下的综合能力。
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
#### 后训练
|
| 38 |
+
- 为进一步提升代码能力与 Agent 任务表现,我们在推理阶段引入 Interleaved Thinking 机制,并通过大量实验验证其在多轮工具交互场景下具备较明显优势;
|
| 39 |
+
- 为提升 Agentic Coding 能力,我们搭建了上万个执行环境,用于合成任务轨迹数据,并支撑 Agentic RL 训练;
|
| 40 |
+
- 为提升 Web Vibe Coding 效果,我们基于 GUI-Agent 构建了一套可自动操作 Web 界面并进行生成结果评估的系统,大幅提升了数据质量。
|
| 41 |
+
|
| 42 |
+
**TeleCoder3-36B-Thinking** 后训练采用迭代式提升方案:
|
| 43 |
+
- 第一阶段:模型冷启动微调,主要提升模型通用代码、Agent、推理、指令理解等能力,为了取得更好的冷启动效果,针对微调数据难度和多样性做了大量筛选工作;
|
| 44 |
+
- 第二阶段:强化学习,采用了基于规则校验奖励和 RM 打分模型融合的方式,对模型综合能力进行进一步强化;
|
| 45 |
+
- 第三阶段:在上一阶段模型基础上,开展长程 Agent 与 Coding 任务的专项微调优化;
|
| 46 |
+
- 第四阶段:主要通过 Agentic RL 训练策略优化模型长程任务能力。
|
| 47 |
+
|
| 48 |
+

|
| 49 |
+
|
| 50 |
+
### 模型结构
|
| 51 |
+
|
| 52 |
+
**TeleCoder3-36B-Thinking** 的模型结构配置和 **TeleChat3-36B-Thinking** 一致:
|
| 53 |
+
|
| 54 |
+
| | Layers | Hidden Size | FFN Intermediate | Attention |
|
| 55 |
+
|------|-----------|------|------|-----|
|
| 56 |
+
| TeleCoder3-36B-Thinking | 64 | 6144 | 24576 | GQA |
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# 能力评估
|
| 60 |
+
**TeleCoder3-36B-Thinking** 在代码综合能力上,不论是 Agentic Coding 还是通用代码类任务均有很好表现,同尺寸模型效果达到最优水平;
|
| 61 |
+
**TeleCoder3-36B-Thinking** 在 Agent 能力上同样表现很好,在 BFCL 和 Tau2-Bench 榜单上达到同级别参数较好水平。
|
| 62 |
+
|
| 63 |
+
| 评测集 | 任务类型 | Kimi-Dev-72B | Qwen3-32B-Thinking | Qwen3-30B-A3B-2507 | Qwen3-Coder-480B-A35B-Instruct | TeleCoder3-36B-Thinking |
|
| 64 |
+
|----------------------------|-------|---------------|--------------------|-----------|--------------|----------------------|
|
| 65 |
+
| IFEval | 指令 | - | **85** | 81.2 | 82.4 | **84.6** |
|
| 66 |
+
| SWE-bench Verified | 代码 | 60.4 | 28 | 51.6 | **69.6** | **65** |
|
| 67 |
+
| Terminal Bench | 代码 | - | 1.2 | 31.3 | **37.5** | **34.8** |
|
| 68 |
+
| Livecodebench(24.08-25.05) | 代码 | 51.5 | 66.7 | 42.5 | 57.5 | **78.2** |
|
| 69 |
+
| ArtifactsBench | 代码 | - | 64.3 | 73.8 | 78.7 | **82.8** |
|
| 70 |
+
| HumanEval-X | 代码 | 67.44 | 84.3 | 88.9 | **91.7** | 88.9 |
|
| 71 |
+
| MBPP-Plus | 代码 | - | **83.9** | 79.9 | 79.4 | **84.2** |
|
| 72 |
+
| CRUXEval | 代码 | - | **95** | 74.1 | 75.9 | 89.7 |
|
| 73 |
+
| BFCL-V3 | Agent | - | **70.3** | 59.2 | 68.7 | **70.5** |
|
| 74 |
+
| Tau2-Bench | Agent | - | 41.7 | 31.2 | 47 | **70** |
|
| 75 |
+
|
| 76 |
+
### 代码场景 Cases
|
| 77 |
+
#### Case1: 大鱼吃小鱼
|
| 78 |
+
**Prompt**
|
| 79 |
+
```markdown
|
| 80 |
+
**游戏名称**: 大鱼吃小鱼
|
| 81 |
+
|
| 82 |
+
**基本功能**:
|
| 83 |
+
1. 游戏界面为水域风格的操作区域,包含核心元素:
|
| 84 |
+
- 玩家小鱼(可操控角色,有基础大小);
|
| 85 |
+
- 其他鱼(不同大小的鱼类,随机分布在操作区域);
|
| 86 |
+
- 基础功能区:显示当前得分、“开始/重新开始”按钮;
|
| 87 |
+
- 简单提示区:文字显示吃小鱼得分、被大鱼吃掉、游戏胜利/失败等反馈。
|
| 88 |
+
|
| 89 |
+
2. 核心交互规则:
|
| 90 |
+
- 操控逻辑:方向键(上下左右)控制玩家小鱼在水域内自由移动,无法超出操作区域边界;
|
| 91 |
+
- 吞噬规则:
|
| 92 |
+
① 吃小鱼:玩家小鱼接触到比自身小的鱼时,判定吞噬成功,被吞噬的鱼消失,玩家小鱼体型变大,得分增加,提示吞噬成功;
|
| 93 |
+
② 被大鱼吃:玩家小鱼接触到比自身大的鱼时,立即判定游戏失败,禁用所有操作,提示“被大鱼吃掉!游戏失败”;
|
| 94 |
+
- 胜利判定:玩家小鱼吞噬足够多的小鱼,体型达到设定的最大尺寸时,判定游戏胜利,禁用所有操作,提示“成为最大的鱼!通关成功”;
|
| 95 |
+
- 鱼类生成:游戏开始后,不同大小的鱼会随机出现在操作区域,数量和移动速度由代码合理控制。
|
| 96 |
+
|
| 97 |
+
3. 基础玩法:
|
| 98 |
+
1. 游戏初始化后,点击“开始”按钮启动游戏,各类鱼开始随机移动;
|
| 99 |
+
2. 玩家通过方向键操控小鱼移动,优先吞噬比自己小的鱼长大,避开比自己大的鱼;
|
| 100 |
+
3. 游戏失败/胜利后,点击“重新开始”可重置小鱼体型、得分和所有鱼类位置,重新游玩。
|
| 101 |
+
|
| 102 |
+
请基于上述要求编写完整HTML代码
|
| 103 |
+
```
|
| 104 |
+
**代码生成效果**
|
| 105 |
+
|
| 106 |
+
<div align="center">
|
| 107 |
+
<img src="./Demo/大鱼吃小鱼.gif" width="50%" alt="大鱼吃小鱼演示">
|
| 108 |
+
<p>👉点击体验 <a href="./Demo/大鱼吃小鱼.html">大鱼吃小鱼</a></p>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
#### Case2: 五子棋
|
| 114 |
+
**Prompt**
|
| 115 |
+
```markdown
|
| 116 |
+
**游戏名称**: 五子棋
|
| 117 |
+
|
| 118 |
+
**基本功能**:
|
| 119 |
+
1. 游戏界面为网格形式的对弈区域,包含核心元素:
|
| 120 |
+
- 空白网格(供落子);
|
| 121 |
+
- 两种不同标识的棋子(区分两位玩家);
|
| 122 |
+
- 基础功能区:显示当前落子方、“重新开始”按钮;
|
| 123 |
+
- 简单提示区:文字显示获胜方、平局(可选)等反馈。
|
| 124 |
+
|
| 125 |
+
2. 核心交互规则:
|
| 126 |
+
- 落子逻辑:
|
| 127 |
+
① 玩家1、玩家2轮流点击空白网格格子落子,各自的棋子有明确区分标识;
|
| 128 |
+
② 已落子的格子无法重复点击,点击无响应;
|
| 129 |
+
- 胜利判定:
|
| 130 |
+
① 任意一方的棋子在网格中连成横、竖、斜向的5子一线,立即判定该玩家获胜,禁用所有落子操作,提示区显示“X玩家获胜!”;
|
| 131 |
+
② 网格全部落满且无5子连线,判定平局,提示区显示“平局!”。
|
| 132 |
+
|
| 133 |
+
3. 基础玩法:
|
| 134 |
+
1. 游戏初始化后,提示区显示“游戏开始!玩家1先落子”;
|
| 135 |
+
2. 两位玩家交替点击空白格子落子,率先将5颗棋子连成一线的玩家获胜;
|
| 136 |
+
3. 游戏获胜/平局后,点击“重新开始”可清空所有棋子,重置游戏状态,重新开始对弈。
|
| 137 |
+
|
| 138 |
+
请基于上述要求编写完整HTML代码
|
| 139 |
+
```
|
| 140 |
+
**代码生成效果**
|
| 141 |
+
<div align="center">
|
| 142 |
+
<img src="./Demo/五子棋.gif" width="50%" alt="五子棋演示">
|
| 143 |
+
<p>👉点击体验 <a href="./Demo/五子棋.html">五子棋</a></p>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
# 推理
|
| 147 |
+
|
| 148 |
+
### Tranformers 推理
|
| 149 |
+
|
| 150 |
+
TeleCoder3 模型支持使用 `transformers` 库进行推理,示例如下:
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
import os
|
| 154 |
+
import torch
|
| 155 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, GenerationConfig
|
| 156 |
+
tokenizer = AutoTokenizer.from_pretrained('./TeleCoder3-36B-Thinking', trust_remote_code=True)
|
| 157 |
+
model = AutoModelForCausalLM.from_pretrained('./TeleCoder3-36B-Thinking', trust_remote_code=True, device_map="auto",torch_dtype=torch.bfloat16)
|
| 158 |
+
prompt = "生抽与老抽的区别?"
|
| 159 |
+
messages = [{"role": "user", "content": prompt}]
|
| 160 |
+
text = tokenizer.apply_chat_template(messages,
|
| 161 |
+
tokenize=False,
|
| 162 |
+
add_generation_prompt=True
|
| 163 |
+
)
|
| 164 |
+
model_inputs = tokenizer(text, return_tensors="pt").to(model.device)
|
| 165 |
+
generated_ids = model.generate(
|
| 166 |
+
**model_inputs,
|
| 167 |
+
top_p=0.95,
|
| 168 |
+
temperature=0.6,
|
| 169 |
+
repetition_penalty=1.0,
|
| 170 |
+
max_new_tokens=16384
|
| 171 |
+
)
|
| 172 |
+
response = tokenizer.decode(generated_ids[0], skip_special_tokens=False,spaces_between_special_tokens=False)
|
| 173 |
+
answer = response.split("</think>")[-1].strip()
|
| 174 |
+
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### VLLM 推理
|
| 178 |
+
|
| 179 |
+
为适配 Interleaved Thinking 推理范式,我们基于 TeleChat3 系列模型重新设计了 Chat Template 及 Tool Call 的拼接方式。因此,在对 TeleCoder3 模型��行推理部署时,请首先参考 [TeleCoder3 vllm 补丁](TeleCoder3_vllm_tool_parser/README.md) 进行 vllm 配置,vllm 版本推荐使用0.9.2。
|
| 180 |
+
|
| 181 |
+
然后,执行以下命令进行模型部署:
|
| 182 |
+
|
| 183 |
+
```shell
|
| 184 |
+
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
|
| 185 |
+
python3 -m vllm.entrypoints.openai.api_server \
|
| 186 |
+
--model TeleCoder3-36B-Thinking \
|
| 187 |
+
--trust-remote-code \
|
| 188 |
+
--tensor-parallel-size 8 \
|
| 189 |
+
--dtype bfloat16 \
|
| 190 |
+
--port 8000 \
|
| 191 |
+
--gpu-memory-utilization 0.9 \
|
| 192 |
+
--uvicorn-log-level "error" \
|
| 193 |
+
--max-model-len 131072 \
|
| 194 |
+
--enable-reasoning \
|
| 195 |
+
--enable-auto-tool-choice \
|
| 196 |
+
--tool-call-parser telechat3 \
|
| 197 |
+
--reasoning-parser deepseek_r1 \
|
| 198 |
+
--disable-custom-all-reduce \
|
| 199 |
+
--chat-template-content-format string
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
然后,您可以与 TeleCoder3 进行对话:
|
| 203 |
+
```python
|
| 204 |
+
from openai import OpenAI
|
| 205 |
+
openai_api_key = "EMPTY"
|
| 206 |
+
openai_api_base = "http://localhost:8000/v1"
|
| 207 |
+
|
| 208 |
+
client = OpenAI(api_key=openai_api_key, base_url=openai_api_base)
|
| 209 |
+
chat_response = client.chat.completions.create(
|
| 210 |
+
model="TeleCoder3-36B-Thinking",
|
| 211 |
+
messages=[
|
| 212 |
+
{"role": "user", "content": "生抽和酱油的区别是什么?"},
|
| 213 |
+
],
|
| 214 |
+
temperature=0.6,
|
| 215 |
+
top_p=0.95,
|
| 216 |
+
max_tokens=16384,
|
| 217 |
+
extra_body={
|
| 218 |
+
"repetition_penalty": 1.0,
|
| 219 |
+
"skip_special_tokens": False,
|
| 220 |
+
"spaces_between_special_tokens": False,
|
| 221 |
+
},
|
| 222 |
+
)
|
| 223 |
+
print("Chat response:", chat_response)
|
| 224 |
+
```
|
| 225 |
+
**注意:** `skip_special_tokens`和`spaces_between_special_tokens`参数必须设置为 False,否则将无法正常解析推理结果
|
| 226 |
+
### 推理参数选择
|
| 227 |
+
- 在推理**代码任务**时,建议使用`repetition_penalty=1.0, top_p=0.95`, `temperature`设为`0.8-1.0`之间的值进行推理。
|
| 228 |
+
- 在推理 **Agent 任务**时,建议使用`repetition_penalty=1.0, temperature=0.6, top_p=0.95`进行推理。
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# 国产化适配
|
| 232 |
+
|
| 233 |
+
### 昇腾 Atlas 800T A2 训练服务器实现训练、推理适配
|
| 234 |
+
|
| 235 |
+
#### 核心组件:
|
| 236 |
+
|
| 237 |
+
- 昇思 MindSpore:该框架是华为开发的深度学习框架,旨在为AI应用提供高效、灵活的开发环境。它支持多种硬件平台,并具有自动微分、模型优化等功能,适合各种深度学习任务。
|
| 238 |
+
|
| 239 |
+
- MindSpore Transformers:该框架的目标是构建一个大模型训练、微调、评估、推理、部署的全流程开发套件,提供业内主流的 Transformer 类预训练模型和 SOTA 下游任务应用,涵盖丰富的并行特性。期望帮助用户轻松的实现大模型训练和创新研发。
|
| 240 |
+
|
| 241 |
+
**当前星辰语义代码大模型 TeleCoder3 支持昇腾 Atlas 800T A2 训练服务器,可基于昇思 MindSpore 框架以及 MindSpore Transformers 框架进行模型训练和评测。如果您对 MindFormers 相关特性有疑问,也可以查看 [MindFormers官方代码和文档](https://gitee.com/mindspore/mindformers/tree/dev/)。**
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# 致谢
|
| 246 |
+
|
| 247 |
+
在此,我们要向开源社区的伟大贡献致以最深切的谢意——正是站在这些巨人的肩膀上,我们才得以眺望更远的风景。
|
| 248 |
+
|
| 249 |
+
特别的向 DeepSeek 团队表达我们诚挚的感激。借鉴其模型架构的设计智慧,为我们模型的训练过程赋予了显著的稳定性与效率,使探索之路更为平稳而清晰。
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
# 声明、协议、引用
|
| 253 |
+
|
| 254 |
+
### 声明
|
| 255 |
+
|
| 256 |
+
我们在此声明,不要使用 TeleCoder 模型及其衍生模型进行任何危害国家社会安全或违法的活动。同时,我们也要求使用者不要将 TeleCoder 模型用于没有安全审查和备案的互联网服务。我们希望所有使用者遵守上述原则,确保科技发展在合法合规的环境下进行。
|
| 257 |
+
|
| 258 |
+
我们已经尽我们所能,来确保模型训练过程中使用的数据的合规性。然而,尽管我们已经做出了巨大的努力,但由于模型和数据的复杂性,仍有可能存在一些无法预见的问题。因此,如果由于使用 TeleCoder 开源模型而导致的任何问题,包括但不限于数据安全问题、公共舆论风险,或模型被误导、滥用、传播或不当利用所带来的任何风险和问题,我们将不承担任何责任。
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
### 引用
|
| 262 |
+
|
| 263 |
+
如需引用我们的工作,请使用如下 reference:
|
| 264 |
+
|
| 265 |
+
```
|
| 266 |
+
@misc{liu2025trainingreporttelechat3moe,
|
| 267 |
+
title={Training Report of TeleChat3-MoE},
|
| 268 |
+
author={Xinzhang Liu and Chao Wang and Zhihao Yang and Zhuo Jiang and Xuncheng Zhao and Haoran Wang and Lei Li and Dongdong He and Luobin Liu and Kaizhe Yuan and Han Gao and Zihan Wang and Yitong Yao and Sishi Xiong and Wenmin Deng and Haowei He and Kaidong Yu and Yu Zhao and Ruiyu Fang and Yuhao Jiang and Yingyan Li and Xiaohui Hu and Xi Yu and Jingqi Li and Yanwei Liu and Qingli Li and Xinyu Shi and Junhao Niu and Chengnuo Huang and Yao Xiao and Ruiwen Wang and Fengkai Li and Luwen Pu and Kaipeng Jia and Fubei Yao and Yuyao Huang and Xuewei He and Zhuoru Jiang and Ruiting Song and Rui Xue and Qiyi Xie and Jie Zhang and Zilu Huang and Zhaoxi Zhang and Zhilong Lu and Yanhan Zhang and Yin Zhang and Yanlei Xue and Zhu Yuan and Teng Su and Xin Jiang and Shuangyong Song and Yongxiang Li and Xuelong Li},
|
| 269 |
+
year={2025},
|
| 270 |
+
eprint={2512.24157},
|
| 271 |
+
archivePrefix={arXiv},
|
| 272 |
+
primaryClass={cs.CL},
|
| 273 |
+
url={https://arxiv.org/abs/2512.24157},
|
| 274 |
+
}
|
| 275 |
+
@misc{wang2025technicalreporttelechat2telechat25,
|
| 276 |
+
title={Technical Report of TeleChat2, TeleChat2.5 and T1},
|
| 277 |
+
author={Zihan Wang and Xinzhang Liu and Yitong Yao and Chao Wang and Yu Zhao and Zhihao Yang and Wenmin Deng and Kaipeng Jia and Jiaxin Peng and Yuyao Huang and Sishi Xiong and Zhuo Jiang and Kaidong Yu and Xiaohui Hu and Fubei Yao and Ruiyu Fang and Zhuoru Jiang and Ruiting Song and Qiyi Xie and Rui Xue and Xuewei He and Yanlei Xue and Zhu Yuan and Zhaoxi Zhang and Zilu Huang and Shiquan Wang and Xin Wang and Hanming Wu and Mingyuan Wang and Xufeng Zhan and Yuhan Sun and Zhaohu Xing and Yuhao Jiang and Bingkai Yang and Shuangyong Song and Yongxiang Li and Zhongjiang He and Xuelong Li},
|
| 278 |
+
year={2025},
|
| 279 |
+
eprint={2507.18013},
|
| 280 |
+
archivePrefix={arXiv},
|
| 281 |
+
primaryClass={cs.CL},
|
| 282 |
+
url={https://arxiv.org/abs/2507.18013},
|
| 283 |
+
}
|
| 284 |
+
```
|
TeleCoder3_vllm_tool_parser/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.egg-info/
|
TeleCoder3_vllm_tool_parser/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
### 针对 VLLM 框架的 TeleCoder3 模型补丁
|
| 2 |
+
|
| 3 |
+
#### 目前支持以下功能
|
| 4 |
+
- 工具调用解析
|
| 5 |
+
- 支持 Interleaved Thinking 模式:chat 接口的 message 支持 reasoning_content 字段,并拼接到 模型上下文中
|
| 6 |
+
|
| 7 |
+
#### 使用教程
|
| 8 |
+
1. 安装插件(只需要执行一次)
|
| 9 |
+
```shell
|
| 10 |
+
pip install -e .
|
| 11 |
+
```
|
| 12 |
+
2. 如果需要工具解析,启动vllm服务的时候加上参数`--enable-auto-tool-choice --tool-call-parser telechat3`
|
TeleCoder3_vllm_tool_parser/setup.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from setuptools import setup, find_packages
|
| 2 |
+
|
| 3 |
+
setup(
|
| 4 |
+
name="telechat3_vllm_patch",
|
| 5 |
+
version="1.1.0",
|
| 6 |
+
packages=find_packages(
|
| 7 |
+
include=["telechat3_tool_parser.py", "telechat3_reasoning.py"]
|
| 8 |
+
),
|
| 9 |
+
entry_points={
|
| 10 |
+
"vllm.general_plugins": [
|
| 11 |
+
"telechat3_tool_parser = telechat3_tool_parser:register_tool_parser",
|
| 12 |
+
"telechat3_reasoning = telechat3_reasoning:register_reasoning",
|
| 13 |
+
]
|
| 14 |
+
},
|
| 15 |
+
)
|
TeleCoder3_vllm_tool_parser/telechat3_reasoning.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import vllm
|
| 2 |
+
from vllm.entrypoints.chat_utils import (
|
| 3 |
+
ChatCompletionMessageParam,
|
| 4 |
+
ConversationMessage,
|
| 5 |
+
BaseMultiModalItemTracker,
|
| 6 |
+
_ChatTemplateContentFormat,
|
| 7 |
+
_parse_chat_message_content,
|
| 8 |
+
)
|
| 9 |
+
from vllm.logger import init_logger
|
| 10 |
+
|
| 11 |
+
logger = init_logger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _telechat3_parse_chat_message_content(
|
| 15 |
+
message: ChatCompletionMessageParam,
|
| 16 |
+
mm_tracker: BaseMultiModalItemTracker,
|
| 17 |
+
content_format: _ChatTemplateContentFormat,
|
| 18 |
+
) -> list[ConversationMessage]:
|
| 19 |
+
result = _parse_chat_message_content(message, mm_tracker, content_format)
|
| 20 |
+
reasoning_content = message.get("reasoning_content")
|
| 21 |
+
|
| 22 |
+
if len(result) > 0 and reasoning_content:
|
| 23 |
+
logger.info("add reasoning content to input prompt.")
|
| 24 |
+
result[0].update({"reasoning_content": reasoning_content})
|
| 25 |
+
|
| 26 |
+
return result
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def register_reasoning():
|
| 30 |
+
vllm.entrypoints.chat_utils._parse_chat_message_content = (
|
| 31 |
+
_telechat3_parse_chat_message_content
|
| 32 |
+
)
|
TeleCoder3_vllm_tool_parser/telechat3_tool_parser.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ast
|
| 2 |
+
import json
|
| 3 |
+
import regex as re
|
| 4 |
+
from collections.abc import Sequence
|
| 5 |
+
from typing import List, Any
|
| 6 |
+
|
| 7 |
+
from transformers import PreTrainedTokenizerBase
|
| 8 |
+
from vllm.entrypoints.openai.protocol import (
|
| 9 |
+
ChatCompletionRequest,
|
| 10 |
+
ChatCompletionToolsParam,
|
| 11 |
+
DeltaFunctionCall,
|
| 12 |
+
DeltaMessage,
|
| 13 |
+
DeltaToolCall,
|
| 14 |
+
ExtractedToolCallInformation,
|
| 15 |
+
FunctionCall,
|
| 16 |
+
ToolCall,
|
| 17 |
+
)
|
| 18 |
+
from vllm.entrypoints.openai.tool_parsers.abstract_tool_parser import (
|
| 19 |
+
ToolParser,
|
| 20 |
+
ToolParserManager,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
from vllm.logger import init_logger
|
| 24 |
+
|
| 25 |
+
logger = init_logger(__name__)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _is_string_type(
|
| 29 |
+
tool_name: str, arg_name: str, tools: List[ChatCompletionToolsParam] | None
|
| 30 |
+
):
|
| 31 |
+
if tools is None:
|
| 32 |
+
return False
|
| 33 |
+
for tool in tools:
|
| 34 |
+
if tool.function.name == tool_name:
|
| 35 |
+
if tool.function.parameters is None:
|
| 36 |
+
return False
|
| 37 |
+
arg_type = (
|
| 38 |
+
tool.function.parameters.get("properties", {})
|
| 39 |
+
.get(arg_name, {})
|
| 40 |
+
.get("type", None)
|
| 41 |
+
)
|
| 42 |
+
return arg_type == "string"
|
| 43 |
+
logger.debug("No tool named '%s'.", tool_name)
|
| 44 |
+
return False
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _deserialize(value: str) -> Any:
|
| 48 |
+
try:
|
| 49 |
+
return json.loads(value)
|
| 50 |
+
except Exception:
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
return ast.literal_eval(value)
|
| 55 |
+
except Exception:
|
| 56 |
+
pass
|
| 57 |
+
return value
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@ToolParserManager.register_module("telechat3")
|
| 61 |
+
class TeleChat3ModelToolParser(ToolParser):
|
| 62 |
+
"""
|
| 63 |
+
Tool call parser for TeleChat3-36B models.
|
| 64 |
+
Used when --enable-auto-tool-choice --tool-call-parser telechat3
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def __init__(self, tokenizer: PreTrainedTokenizerBase):
|
| 68 |
+
super().__init__(tokenizer)
|
| 69 |
+
|
| 70 |
+
# initialize properties used for state when parsing tool calls in
|
| 71 |
+
# streaming mode
|
| 72 |
+
self.current_tool_id: int = -1
|
| 73 |
+
|
| 74 |
+
self.tool_start_token = "<tool_call>"
|
| 75 |
+
self.tool_end_token = "</tool_call>"
|
| 76 |
+
|
| 77 |
+
self.func_detail_regex = re.compile(
|
| 78 |
+
r"<tool_call>(.*?)(<param_key>.*?)?</tool_call>", re.DOTALL
|
| 79 |
+
)
|
| 80 |
+
self.func_arg_regex = re.compile(
|
| 81 |
+
r"<param_key>(.*?)</param_key>(?:\\n|\s)*<param_value>(.*?)</param_value>",
|
| 82 |
+
re.DOTALL,
|
| 83 |
+
)
|
| 84 |
+
self._buffer = ""
|
| 85 |
+
|
| 86 |
+
def extract_tool_calls(self, model_output: str, request: ChatCompletionRequest):
|
| 87 |
+
|
| 88 |
+
matched_tool_calls = self.func_detail_regex.findall(model_output)
|
| 89 |
+
logger.debug("model_output: %s", model_output)
|
| 90 |
+
|
| 91 |
+
tool_calls = []
|
| 92 |
+
try:
|
| 93 |
+
for match in matched_tool_calls:
|
| 94 |
+
tc_name = match[0].strip()
|
| 95 |
+
arg_dict = {}
|
| 96 |
+
if len(match) > 1:
|
| 97 |
+
for key, value in self.func_arg_regex.findall(match[1]):
|
| 98 |
+
arg_key = key.strip()
|
| 99 |
+
arg_val = value.strip()
|
| 100 |
+
if not _is_string_type(tc_name, key, request.tools):
|
| 101 |
+
arg_val = _deserialize(arg_val)
|
| 102 |
+
logger.debug("arg_key = %s, arg_val = %s", arg_key, arg_val)
|
| 103 |
+
arg_dict[arg_key] = arg_val
|
| 104 |
+
tool_calls.append(
|
| 105 |
+
ToolCall(
|
| 106 |
+
type="function",
|
| 107 |
+
function=FunctionCall(
|
| 108 |
+
name=tc_name,
|
| 109 |
+
arguments=json.dumps(arg_dict, ensure_ascii=False),
|
| 110 |
+
),
|
| 111 |
+
)
|
| 112 |
+
)
|
| 113 |
+
except Exception:
|
| 114 |
+
logger.exception("Failed to extract tool call spec")
|
| 115 |
+
return ExtractedToolCallInformation(
|
| 116 |
+
tools_called=False, tool_calls=[], content=model_output
|
| 117 |
+
)
|
| 118 |
+
else:
|
| 119 |
+
if len(tool_calls) > 0:
|
| 120 |
+
content = model_output[: model_output.find(self.tool_start_token)]
|
| 121 |
+
return ExtractedToolCallInformation(
|
| 122 |
+
tools_called=True, tool_calls=tool_calls, content=content
|
| 123 |
+
)
|
| 124 |
+
return ExtractedToolCallInformation(
|
| 125 |
+
tools_called=False, tool_calls=[], content=model_output
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def extract_tool_calls_streaming(
|
| 129 |
+
self,
|
| 130 |
+
previous_text: str,
|
| 131 |
+
current_text: str,
|
| 132 |
+
delta_text: str,
|
| 133 |
+
previous_token_ids: Sequence[int],
|
| 134 |
+
current_token_ids: Sequence[int],
|
| 135 |
+
delta_token_ids: Sequence[int],
|
| 136 |
+
request: ChatCompletionRequest,
|
| 137 |
+
) -> DeltaMessage | None:
|
| 138 |
+
self._buffer += delta_text
|
| 139 |
+
cur_text = self._buffer
|
| 140 |
+
start_idx = cur_text.find(self.tool_start_token)
|
| 141 |
+
if start_idx == -1:
|
| 142 |
+
self._buffer = ""
|
| 143 |
+
return DeltaMessage(content=cur_text)
|
| 144 |
+
logger.debug("cur_text = %s", cur_text)
|
| 145 |
+
end_idx = cur_text.find(self.tool_end_token)
|
| 146 |
+
if end_idx != -1:
|
| 147 |
+
extracted_tool_calls = self.extract_tool_calls(
|
| 148 |
+
cur_text[: end_idx + len(self.tool_end_token)], request
|
| 149 |
+
)
|
| 150 |
+
if len(extracted_tool_calls.tool_calls) == 0:
|
| 151 |
+
logger.warning("Failed to extract any tool calls.")
|
| 152 |
+
return None
|
| 153 |
+
self.current_tool_id += 1
|
| 154 |
+
tool_call = extracted_tool_calls.tool_calls[0]
|
| 155 |
+
delta = DeltaMessage(
|
| 156 |
+
content=extracted_tool_calls.content,
|
| 157 |
+
tool_calls=[
|
| 158 |
+
DeltaToolCall(
|
| 159 |
+
index=self.current_tool_id,
|
| 160 |
+
id=tool_call.id,
|
| 161 |
+
type=tool_call.type,
|
| 162 |
+
function=DeltaFunctionCall(
|
| 163 |
+
name=tool_call.function.name,
|
| 164 |
+
arguments=tool_call.function.arguments,
|
| 165 |
+
),
|
| 166 |
+
)
|
| 167 |
+
],
|
| 168 |
+
)
|
| 169 |
+
self._buffer = cur_text[end_idx + len(self.tool_end_token) :]
|
| 170 |
+
return delta
|
| 171 |
+
self._buffer = cur_text[start_idx:]
|
| 172 |
+
return DeltaMessage(content=cur_text[:start_idx])
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def register_tool_parser(): ...
|
images/TeleCoder3_post_training.png
ADDED
|