├── .gitignore ├── README.md ├── app.js ├── client ├── css │ ├── app.css │ ├── doc.css │ ├── editor.css │ ├── element.css │ ├── game.css │ ├── index.css │ ├── map-editor.css │ ├── rank.css │ ├── tournament.css │ ├── user.css │ ├── vendor │ │ └── bracket.css │ └── vs.css ├── images │ ├── app.sketch │ ├── bullet1.png │ ├── bullet2.png │ ├── crash1.png │ ├── crash2.png │ ├── crash3.png │ ├── doc │ │ └── map.png │ ├── favicon.ico │ ├── grass.png │ ├── ground.png │ ├── header-logo.png │ ├── logo.png │ ├── share │ │ ├── twitter.png │ │ └── weibo.png │ ├── smoke1.png │ ├── smoke2.png │ ├── smoke3.png │ ├── star.png │ ├── star_small.png │ ├── start-btn-hover.png │ ├── start-btn.png │ ├── stone.png │ ├── tank1.png │ ├── tank1_small.png │ ├── tank2.png │ └── tank2_small.png └── js │ ├── app.js │ ├── editor.js │ ├── editor │ └── template.js │ ├── game │ ├── editor.js │ ├── index.js │ ├── map.js │ └── records-parser.js │ ├── history.js │ ├── map-editor.js │ ├── tournament.js │ ├── vendor │ └── bracket.js │ └── vs.js ├── config └── _sample.json ├── env.js ├── gulpfile.js ├── models ├── code.js ├── history.js ├── index.js ├── map.js ├── result.js ├── tournament.js └── user.js ├── package.json ├── routes ├── account.js ├── code.js ├── defaults.js ├── doc.js ├── history.js ├── index.js ├── map.js ├── rank.js ├── replay.js └── tournaments.js ├── sandbox ├── game │ ├── commander.js │ ├── game.js │ ├── movable.js │ ├── player.js │ ├── replay.js │ └── sandbox.js └── index.js ├── services ├── calc_rank.js ├── calc_tournament.js └── util.js └── views ├── doc ├── api.jade ├── rule.jade └── tournament.jade ├── editor.jade ├── error.jade ├── history.jade ├── include └── header.jade ├── index.jade ├── layout.jade ├── map ├── edit.jade └── index.jade ├── rank.jade ├── tournaments ├── index.jade └── show.jade ├── user.jade └── vs.jade /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | 4 | public 5 | 6 | config/* 7 | !config/_sample.json 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Game 2 | 3 | Code GAME 是一个通过编写 AI 脚本控制坦克进行比赛的游戏。 4 | 5 | 娱乐第一,请点右上角 star! 6 | 7 | ## 安装 8 | 9 | $ npm install 10 | $ npm run build 11 | $ cp config/_sample.json config/development.json 12 | $ vim config/development.json 13 | $ node app 14 | 15 | 需要软件版本: 16 | 17 | Node v0.11.12 或更高版本 18 | Redis v2.4 或更高版本 19 | MySQL 20 | 21 | ## 配置 22 | 23 | 1. GitHub 配置 24 | 在 GitHub [创建新应用](https://github.com/settings/applications/new),在 Authorization callback URL 栏目填写 http://127.0.0.1:3000/account/github/callback。 然后将应用的 key 和 secret 填到 config/development.json 里的相应位置 25 | 26 | 2. 地图 27 | 地图信息存储在数据库的 Maps 表中,其中 data 为地图信息,x 表示石头,o 表示草坪,. 表示空地。abcd 表示玩家 1 的出生点(分别对应初始朝向 上 右 下 左),ABCD 表示玩家 2 的出生点。以 | 分隔每行。默认的地图为: 28 | 29 | xxxxxxxxxxxxxxxxxxx|xooo..............x|xoax.....x........x|xxxx.....x........x|x........x........x|x..xxxxxxxxxxx....x|x....ooxoooooo....x|x....ooooooooo....x|x....ooooooxoo....x|x....xxxxxxxxxxx..x|x........x........x|x........x.....xxxx|x........x.....xCox|x..............ooox|xxxxxxxxxxxxxxxxxxx 30 | 31 | ## 计算排行 32 | 33 | node services/calc_rank.js 34 | 35 | ## 技术栈 36 | 37 | Node.js, MySQL 38 | 39 | ## 介绍文章 40 | 41 | [Code Game 对技术的选取——兼谈为何不应该用 CoffeeScript 与 Less](http://zihua.li/2014/11/talk-about-codegame/) 42 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('./env'); 2 | 3 | var express = require('express'); 4 | var path = require('path'); 5 | var app = express(); 6 | var config = require('config'); 7 | 8 | app.set('port', process.env.PORT || 3000); 9 | 10 | // view engine setup 11 | app.set('views', path.join(__dirname, 'views')); 12 | app.set('view engine', 'jade'); 13 | 14 | app.use(require('morgan')('dev')); 15 | 16 | var bodyParser = require('body-parser'); 17 | app.use(bodyParser.json()); 18 | app.use(bodyParser.urlencoded()); 19 | 20 | app.use(require('cookie-parser')()); 21 | 22 | var session = require('express-session'); 23 | var RedisStore = require('connect-redis')(session); 24 | app.use(session({ 25 | store: new RedisStore(config.redis), 26 | secret: config.session.secret, 27 | cookie: { 28 | maxAge: 60000 * 60 * 24 * 14 29 | } 30 | })); 31 | 32 | app.use(function(req, res, next) { 33 | res.locals.config = config; 34 | res.locals.req = req; 35 | next(); 36 | }); 37 | 38 | app.use('/public', express.static(path.join(__dirname, 'public'))); 39 | 40 | require('./routes')(app); 41 | 42 | /// catch 404 and forwarding to error handler 43 | app.use(function(req, res, next) { 44 | var err = new Error('Not Found'); 45 | err.status = 404; 46 | next(err); 47 | }); 48 | 49 | /// error handlers 50 | 51 | // development error handler 52 | // will print stacktrace 53 | if (app.get('env') === 'development') { 54 | app.use(function(err, req, res, next) { 55 | res.status(err.status || 500); 56 | res.render('error', { 57 | message: err.message, 58 | error: err 59 | }); 60 | }); 61 | } 62 | 63 | // production error handler 64 | // no stacktraces leaked to user 65 | app.use(function(err, req, res, next) { 66 | res.status(err.status || 500); 67 | res.render('error', { 68 | message: err.message, 69 | error: err 70 | }); 71 | }); 72 | 73 | require('http').createServer(app).listen(app.get('port'), function() { 74 | console.log('Express server listening on port ' + app.get('port')); 75 | }); 76 | -------------------------------------------------------------------------------- /client/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | color: #403c2f; 5 | font-family: "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 6 | } 7 | 8 | button { 9 | outline: none; 10 | } 11 | 12 | #preload { display: none; } 13 | 14 | a { color: #68bb68; text-decoration: none; } 15 | a:hover { color: #568a56; transition: 0.5s; } 16 | 17 | header.top { 18 | position: relative; 19 | margin: 0; 20 | padding: 0 20px; 21 | background-color: #f1f1f1; 22 | border-bottom: 1px solid #dcdcdc; 23 | overflow: hidden; 24 | } 25 | 26 | header .ad { 27 | padding: 4px 8px; 28 | border-radius: 4px; 29 | background: #e7c251; 30 | font-size: 12px; 31 | color: #fff; 32 | -webkit-transition: 0.2s; 33 | transition: 0.2s; 34 | } 35 | 36 | header .ad:hover { 37 | background: #d9b139; 38 | } 39 | 40 | header .ad a { 41 | color: #fff; 42 | } 43 | 44 | .wrap { 45 | overflow: hidden; 46 | height: auto; 47 | width: 100%; 48 | position: absolute; 49 | top: 57px; 50 | bottom: 0; 51 | } 52 | 53 | .wrap.is-dark { 54 | background: #202020; 55 | } 56 | 57 | .top .container { 58 | width: 980px; 59 | margin: 0 auto; 60 | overflow: hidden; 61 | } 62 | 63 | .top ul { 64 | padding: 0; 65 | list-style: none; 66 | } 67 | 68 | .top ul li { 69 | float: left; 70 | } 71 | 72 | .top ul.pull-right li { 73 | margin-left: 20px; 74 | } 75 | 76 | .top ul.pull-left li { 77 | margin-right: 20px; 78 | } 79 | 80 | .top ul.pull-right { 81 | float: right; 82 | } 83 | 84 | .top ul.pull-left { 85 | float: left; 86 | } 87 | 88 | .header-logo { 89 | position: absolute; 90 | padding: 14px 0; 91 | left: 50%; 92 | top: 0; 93 | margin-left: -87px; 94 | } 95 | 96 | @import "element.css"; 97 | @import "index.css"; 98 | @import "user.css"; 99 | @import "editor.css"; 100 | @import "vs.css"; 101 | @import "game.css"; 102 | @import "doc.css"; 103 | @import "rank.css"; 104 | @import "tournament.css"; 105 | @import "map-editor.css"; 106 | -------------------------------------------------------------------------------- /client/css/doc.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 80%; 3 | margin: 50px auto; 4 | } 5 | 6 | aside { 7 | width: 30%; 8 | float: left; 9 | } 10 | article { 11 | position: relative; 12 | top: -40px; 13 | width: 70%; 14 | float: right; 15 | } 16 | 17 | article h1, 18 | article h2, 19 | article h3, 20 | article h4, 21 | article h5, 22 | article h6 { 23 | text-transform: uppercase; 24 | font-weight: bold; 25 | } 26 | 27 | article h1 code, 28 | article h2 code, 29 | article h3 code, 30 | article h4 code, 31 | article h5 code, 32 | article h6 code { 33 | text-transform: none; 34 | } 35 | 36 | article h2 { 37 | margin: 60px 0 0; 38 | font-size: 30px; 39 | line-height: 30px; 40 | } 41 | 42 | article h2 span { 43 | color: #ccc; 44 | font-size: 14px; 45 | display: block; 46 | font-weight: normal; 47 | } 48 | 49 | article .center { 50 | } 51 | 52 | article ul.elements { 53 | margin: 20px 0 20px 0; 54 | padding: 0; 55 | list-style: none; 56 | overflow: hidden; 57 | } 58 | 59 | article ul.elements li { 60 | position: relative; 61 | min-height: 70px; 62 | margin-bottom: 10px; 63 | float: left; 64 | width: 45%; 65 | } 66 | 67 | article ul.elements li:nth-child(odd) { 68 | margin-right: 10%; 69 | } 70 | 71 | article ul.elements img { 72 | position: absolute; 73 | left: 0; 74 | top: 0; 75 | } 76 | 77 | article ul.elements p { 78 | margin: 0; 79 | padding-left: 70px; 80 | color: #999; 81 | } 82 | 83 | article table { 84 | border-spacing: 0; 85 | border-collapse: collapse; 86 | margin: 20px 0; 87 | } 88 | 89 | article table td { 90 | } 91 | 92 | article table th { 93 | border-bottom: 2px solid #ddd; 94 | } 95 | 96 | article table td, 97 | article table th { 98 | text-align: left; 99 | vertical-align: top; 100 | padding: 8px; 101 | } 102 | 103 | article table tbody tr:nth-child(odd) { 104 | border-top: 1px solid #ccc; 105 | background: #f9f9f9; 106 | } 107 | -------------------------------------------------------------------------------- /client/css/editor.css: -------------------------------------------------------------------------------- 1 | @import "../../node_modules/codemirror/lib/codemirror.css"; 2 | @import "../../node_modules/codemirror/theme/ambiance.css"; 3 | @import "../../node_modules/codemirror/addon/lint/lint.css"; 4 | 5 | html, body { height:100%; } 6 | 7 | .CodeMirror { 8 | font-family: 'Source Code Pro'; 9 | line-height: 1.2; 10 | font-size: 14px; 11 | height: 100%; 12 | } 13 | 14 | .CodeMirror-activeline-background { 15 | background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.131) !important; 16 | } 17 | 18 | .toolbar { 19 | position: fixed; 20 | width: 500px; 21 | right: 14px; 22 | bottom: 10px; 23 | overflow: hidden; 24 | margin-bottom: 10px; 25 | z-index: 999; 26 | } 27 | 28 | .toolbar .preview-options { 29 | float: left; 30 | overflow: hidden; 31 | height: 34px; 32 | padding: 4px 4px 0; 33 | border-radius: 4px; 34 | background: rgba(255, 255, 255, 0.2); 35 | width: 430px; 36 | } 37 | 38 | .preview-options button.preview-btn { 39 | float: right; 40 | margin-right: 4px; 41 | } 42 | 43 | .toolbar input { 44 | width: 180px; 45 | background: rgba(0, 0, 0, 0.5); 46 | border: none; 47 | padding: 8px 16px; 48 | border-radius: 6px; 49 | float: left; 50 | outline: none; 51 | color: #fff; 52 | } 53 | 54 | .toolbar select { 55 | background: rgba(0, 0, 0, 0.5); 56 | border: none; 57 | margin-left: 4px; 58 | height: 28px; 59 | color: #fff; 60 | border-radius: 6px; 61 | outline: none; 62 | width: 75px; 63 | float: left; 64 | } 65 | 66 | .toolbar .item { 67 | float: right; 68 | margin-top: 4px; 69 | } 70 | 71 | .toolbar button { 72 | padding: 8px 16px; 73 | border-radius: 6px; 74 | color: #fff; 75 | background: #68bb68; 76 | } 77 | 78 | #editor { 79 | width: 100%; 80 | height: 100%; 81 | } 82 | 83 | .playground-container { 84 | position: fixed; 85 | width: 500px; 86 | height: 490px; 87 | right: 14px; 88 | bottom: 10px; 89 | z-index: 999; 90 | } 91 | 92 | .js-playground { 93 | visibility: hidden; 94 | } 95 | 96 | .js-close-playground { 97 | position: absolute; 98 | right: 0; 99 | top: 0; 100 | background: rgba(255, 255, 255, 0.5); 101 | width: 16px; 102 | height: 16px; 103 | cursor: pointer; 104 | line-height: 16px; 105 | text-align: center; 106 | color: #444; 107 | z-index: 999; 108 | font-size: 12px; 109 | } 110 | 111 | #playground { 112 | border-radius: 10px; 113 | overflow: hidden; 114 | } 115 | -------------------------------------------------------------------------------- /client/css/element.css: -------------------------------------------------------------------------------- 1 | .button { 2 | cursor: pointer; 3 | position: relative; 4 | color: #fff; 5 | display: block; 6 | text-decoration: none; 7 | margin: 0 auto; 8 | border-radius: 2px; 9 | border: none; 10 | border-bottom: solid 5px #4b8e4b; 11 | background: #68bb68; 12 | text-align: center; 13 | padding: 10px 20px; 14 | transition: all 0.1s; 15 | font-size: 18px; 16 | } 17 | 18 | .button:not(.is-disabled):active{ 19 | border-bottom: solid 1px #4b8e4b; 20 | position: relative; 21 | top: 4px; 22 | } 23 | 24 | .button.small-button { 25 | font-size: 14px; 26 | padding: 5px 10px; 27 | border-bottom: solid 3px #4b8e4b; 28 | } 29 | 30 | .button:not(.is-disabled).small-button:active { 31 | border-bottom: solid 1px #4b8e4b; 32 | top: 2px; 33 | } 34 | 35 | .button.is-disabled { 36 | background-color: #c0c5c0; 37 | border-bottom-color: #a1a3a1; 38 | } 39 | -------------------------------------------------------------------------------- /client/css/game.css: -------------------------------------------------------------------------------- 1 | @keyframes star { 2 | 0% { transform: rotate(0deg); } 3 | 12% { transform: rotate(90deg); } 4 | 24% { transform: rotate(180deg); } 5 | 36% { transform: rotate(270deg); } 6 | 48% { transform: rotate(360deg); } 7 | 100% { transform: rotate(360deg); } 8 | } 9 | 10 | @keyframes crash { 11 | from { opacity: 1; transform: scale(1); } 12 | 70% { opacity: 0.9; transform: scale(1.5); } 13 | to { opacity: 0; transform: scale(1.8); } 14 | } 15 | 16 | @keyframes crashed { 17 | from { opacity: 1; } 18 | to { opacity: 0; } 19 | } 20 | 21 | .playground .crashed { 22 | animation: crashed 1s linear 0s forwards; 23 | } 24 | 25 | .playground { 26 | position: relative; 27 | background: #e7dfc2 url(../images/ground.png); 28 | background-size: 50px 50px; 29 | transform-origin: left top; 30 | } 31 | 32 | .playground .stone { 33 | z-index: 1; 34 | position: absolute; 35 | width: 50px; 36 | height: 50px; 37 | background: transparent url(../images/stone.png) left top no-repeat; 38 | background-size: 100%; 39 | } 40 | 41 | .playground .grass { 42 | z-index: 3; 43 | position: absolute; 44 | width: 50px; 45 | height: 50px; 46 | background: transparent url(../images/grass.png) left top no-repeat; 47 | background-size: 100%; 48 | } 49 | 50 | .playground .star { 51 | z-index: 4; 52 | position: absolute; 53 | overflow: hidden; 54 | background: transparent url(../images/star.png) center center no-repeat; 55 | background-size: 100%; 56 | width: 50px; 57 | height: 50px; 58 | animation: star 10s linear 2s infinite; 59 | } 60 | 61 | .playground .player { 62 | z-index: 2; 63 | position: absolute; 64 | width: 50px; 65 | height: 50px; 66 | background: transparent url(../images/tank1.png) left top no-repeat; 67 | background-size: 100%; 68 | } 69 | 70 | .playground .player.player2 { 71 | background-image: url(../images/tank2.png); 72 | } 73 | 74 | .playground .bullet { 75 | z-index: 1; 76 | position: absolute; 77 | width: 50px; 78 | height: 50px; 79 | background: transparent url(../images/bullet1.png) left top no-repeat; 80 | background-size: 100%; 81 | } 82 | 83 | .playground .crash { 84 | z-index: 4; 85 | position: absolute; 86 | width: 50px; 87 | height: 50px; 88 | background: transparent url(../images/crash1.png) left top no-repeat; 89 | background-size: 100%; 90 | } 91 | 92 | .playground .crash.play { 93 | animation: crash 1s linear 0s forwards; 94 | } 95 | 96 | .playground .bullet.bullet2 { 97 | background-image: url(../images/bullet2.png); 98 | } 99 | 100 | .playground .status { 101 | position: absolute; 102 | bottom: 0; 103 | left: 0; 104 | width: 100%; 105 | height: 50px; 106 | padding-top: 10px; 107 | background-color: #f1f1f1; 108 | user-select: none; 109 | -webkit-user-select: none; 110 | } 111 | 112 | .playground .star-bar { 113 | height: 40px; 114 | overflow: hidden; 115 | } 116 | 117 | .playground .player-bar { 118 | height: 40px; 119 | overflow: hidden; 120 | } 121 | 122 | .playground .player1-bar { 123 | float: left; 124 | background: transparent url(../images/tank1_small.png) left center no-repeat; 125 | padding-left: 50px; 126 | background-size: 40px 40px; 127 | } 128 | 129 | .playground .player2-bar { 130 | float: right; 131 | padding-right: 50px; 132 | background: transparent url(../images/tank2_small.png) right center no-repeat; 133 | background-size: 40px 40px; 134 | } 135 | 136 | .playground .player-bar .star-count { 137 | position: relative; 138 | top: 1px; 139 | font-family: 'Source Code Pro'; 140 | text-align: center; 141 | color: #EBB12B; 142 | width: 40px; 143 | line-height: 38px; 144 | height: 40px; 145 | background: transparent url(../images/star_small.png) left center no-repeat; 146 | background-size: 40px 40px; 147 | } 148 | 149 | .playground .player1-bar .star-count { 150 | float: left; 151 | } 152 | 153 | .playground .player2-bar .star-count { 154 | float: right; 155 | } 156 | 157 | .playground .player-bar .name { 158 | line-height: 45px; 159 | } 160 | 161 | .playground .player1-bar .name { 162 | float: left; 163 | margin-left: 10px; 164 | } 165 | 166 | .playground .player2-bar .name { 167 | float: right; 168 | margin-right: 10px; 169 | } 170 | 171 | .playground .frames { 172 | position: absolute; 173 | left: 0; 174 | width: 100%; 175 | height: 40px; 176 | top: 10px; 177 | font-weight: bold; 178 | text-align: center; 179 | line-height: 40px; 180 | font-size: 18px; 181 | cursor: default; 182 | } 183 | 184 | .playground .window { 185 | z-index: 9999; 186 | position: absolute; 187 | top: 0; 188 | left: 0; 189 | width: 100%; 190 | height: calc(100% - 60px); 191 | background-color: rgba(0, 0, 0, 0.7); 192 | } 193 | 194 | .playground .modal { 195 | position: absolute; 196 | border-radius: 8px; 197 | left: 50%; 198 | top: 50%; 199 | width: 400px; 200 | height: 400px; 201 | margin-left: -200px; 202 | margin-top: -200px; 203 | z-index: 99999; 204 | background-color: #1ea7e1; 205 | border: 2px solid #1989B8; 206 | -webkit-box-shadow:inset 1px 1px 1px 1px #35BAF3; 207 | box-shadow:inset 1px 1px 1px 1px #35BAF3; 208 | text-align: center; 209 | } 210 | 211 | .playground .modal .content { 212 | position: absolute; 213 | top: 40px; 214 | height: 360px; 215 | width: 100%; 216 | margin-left: -2px; 217 | border-radius: 8px; 218 | background-color: #eeeeee; 219 | border: 2px solid #aca5a2; 220 | -webkit-box-shadow:inset 1px 1px 1px 1px #ffffff; 221 | box-shadow:inset 1px 1px 1px 1px #ffffff; 222 | } 223 | 224 | .playground .modal .content p { 225 | margin: 0 auto 10px; 226 | } 227 | 228 | .playground .modal .title { 229 | color: #e2f9ff; 230 | line-height: 40px; 231 | font-size: 18px; 232 | font-weight: bold; 233 | } 234 | 235 | .playground .modal .content p.section-title { 236 | margin: 20px auto 2px; 237 | font-weight: bold; 238 | } 239 | 240 | .playground .modal .winner-img { 241 | margin: 0 auto; 242 | width: 50px; 243 | height: 50px; 244 | background-color: transparent; 245 | background-repeat: no-repeat; 246 | background-size: 100% 100%; 247 | } 248 | 249 | .playground .modal .winner-img0 { 250 | background-image: url(../images/tank1_small.png); 251 | } 252 | 253 | .playground .modal .winner-img1 { 254 | background-image: url(../images/tank2_small.png); 255 | } 256 | 257 | .playground .modal button { 258 | margin-top: 20px; 259 | padding: 8px 26px; 260 | border: none; 261 | color: #fff; 262 | background: #68bb68; 263 | font-size: 18px; 264 | border-radius: 4px; 265 | } 266 | -------------------------------------------------------------------------------- /client/css/index.css: -------------------------------------------------------------------------------- 1 | .index { 2 | text-align: center; 3 | } 4 | 5 | .logo { 6 | margin: 120px auto 0; 7 | } 8 | 9 | .desc { 10 | margin: 50px auto; 11 | font-size: 18px; 12 | font-weight: normal; 13 | } 14 | 15 | button.start { 16 | display: block; 17 | border: none; 18 | margin: 0 auto; 19 | width: 254px; 20 | height: 61px; 21 | background: transparent url(../images/start-btn.png) no-repeat; 22 | background-size: 100%; 23 | transition: background 0.5s; 24 | } 25 | 26 | button.start:hover { 27 | background-image: url(../images/start-btn-hover.png); 28 | } 29 | -------------------------------------------------------------------------------- /client/css/map-editor.css: -------------------------------------------------------------------------------- 1 | #map-editor { 2 | position: relative; 3 | /* -webkit-transition: 0.2s; 4 | transition: 0.2s;*/ 5 | } -------------------------------------------------------------------------------- /client/css/rank.css: -------------------------------------------------------------------------------- 1 | .rank-section { 2 | text-align: center; 3 | } 4 | 5 | .rank-section span { 6 | font-size: 14px; 7 | font-weight: normal; 8 | color: #ccc; 9 | display: block; 10 | } 11 | 12 | .rank-section table { 13 | margin: 10px auto; 14 | width: 700px; 15 | } 16 | 17 | .rank-section td { 18 | vertical-align: top; 19 | } 20 | -------------------------------------------------------------------------------- /client/css/tournament.css: -------------------------------------------------------------------------------- 1 | .tournament { 2 | margin: 50px auto; 3 | width: 600px; 4 | text-align: center; 5 | } 6 | 7 | .tournament-description { 8 | margin: 20px; 9 | padding: 10px; 10 | font-size: 12px; 11 | text-align: left; 12 | background: #efefef; 13 | } 14 | 15 | .tournament-action { 16 | margin: 30px 0; 17 | } 18 | 19 | ul.tournament-users { 20 | padding: 0; 21 | margin: 0; 22 | list-style: none; 23 | overflow: hidden; 24 | } 25 | 26 | ul.tournament-users li { 27 | float: left; 28 | margin: 0 10px 10px 0; 29 | } 30 | 31 | ul.tournament-users img { 32 | border-radius: 32px; 33 | } 34 | 35 | @import "vendor/bracket.css"; 36 | -------------------------------------------------------------------------------- /client/css/user.css: -------------------------------------------------------------------------------- 1 | .user-info { 2 | text-align: center; 3 | } 4 | 5 | .user-info .avatar { 6 | margin: 30px auto 10px; 7 | } 8 | 9 | .user-info .avatar img { 10 | border-radius: 1000px; 11 | width: 84px; 12 | height: 84px; 13 | } 14 | 15 | .user-info .name { 16 | margin: 0 0 10px; 17 | font-family: 'Source Code Pro'; 18 | font-size: 30px; 19 | font-weight: 700; 20 | text-transform: uppercase; 21 | color: #403c2f; 22 | } 23 | 24 | .user-info .bio { 25 | font-family: 'Source Code Pro'; 26 | color: #7f7f7f; 27 | font-size: 20px; 28 | } 29 | 30 | .rank { 31 | margin: 0 auto 40px; 32 | line-height: 68px; 33 | height: 68px; 34 | width: 68px; 35 | text-align: center; 36 | background-color: #f9d123; 37 | border: 4px solid #3b3b39; 38 | border-radius: 100px; 39 | color: #fff; 40 | -webkit-user-select: none; 41 | user-select: none; 42 | } 43 | -------------------------------------------------------------------------------- /client/css/vendor/bracket.css: -------------------------------------------------------------------------------- 1 | /* jQuery Bracket | Copyright (c) Teijo Laine 2011-2013 | Licenced under the MIT licence */ 2 | div.jQBracket{font-family:Arial;font-size:14px;position:relative}div.jQBracket .tools{position:absolute;top:0;color:#fff}div.jQBracket .tools span{cursor:pointer;margin:5px;display:block;text-align:center;width:18px;height:18px;background-color:#666}div.jQBracket .tools span:hover{background-color:#999}div.jQBracket .finals{float:right;right:0;clear:right;position:relative}div.jQBracket .bracket{float:right;clear:left}div.jQBracket .loserBracket{float:right;clear:left;position:relative}div.jQBracket .round{position:relative;width:100px;margin-right:40px;float:left}div.jQBracket .match{position:relative}div.jQBracket .editable{cursor:pointer}div.jQBracket .team{position:relative;z-index:1;float:left;background-color:#eee;width:100px;cursor:default}div.jQBracket .team:first-child{border-bottom:1px solid #999}div.jQBracket .team input{font-size:12px;padding:0;width:inherit;border:0;margin:0}div.jQBracket .team div.label{padding:3px;position:absolute;width:70px;height:22px;white-space:nowrap;overflow:hidden}div.jQBracket .team div.label[disabled]{cursor:default}div.jQBracket .team div.score{float:right;padding:3px;background-color:rgba(255,255,255,.3);text-align:center;width:20px}div.jQBracket .team div.score[disabled]{color:#999;cursor:default}div.jQBracket .team div.label input.error,div.jQBracket .team div.score input.error{background-color:#fcc}div.jQBracket .team.np{background-color:#666;color:#eee}div.jQBracket .team.na{background-color:#999;color:#ccc}div.jQBracket .team.win{color:#333}div.jQBracket .team.win div.score{color:#060}div.jQBracket .team.lose div.score{color:#900}div.jQBracket .team.lose{background-color:#ddd;color:#999}div.jQBracket .team.tie div.score{color:#00f}div.jQBracket .team.highlightWinner{background-color:#da0;color:#000}div.jQBracket .team.highlightLoser{background-color:#ccc;color:#000}div.jQBracket .team.highlight{background-color:#3c0;color:#000}div.jQBracket .teamContainer{z-index:1;position:relative;float:left}div.jQBracket .connector{border:2px solid #666;border-left-style:none;position:absolute;z-index:1}div.jQBracket .connector div.connector{border:0;border-bottom:2px solid #666;height:0;position:absolute}div.jQBracket .connector.highlightWinner,div.jQBracket .connector div.connector.highlightWinner{border-color:#da0}div.jQBracket .connector.highlightLoser,div.jQBracket .connector div.connector.highlightLoser{border-color:#ccc}div.jQBracket .connector.highlight,div.jQBracket .connector div.connector.highlight{border-color:#0c0}div.jQBracket .np .connector,div.jQBracket .np .connector div.connector{border-color:#222}div.jQBracket .bubble{height:22px;line-height:22px;width:30px;right:-35px;position:absolute;text-align:center;font-size:11px}div.jQBracket .bubble.third{background-color:#963;color:#d95}div.jQBracket .bubble.fourth{background-color:#678;color:#ccd}div.jQBracket .bubble:after{content:"";position:absolute;top:6px;width:0;height:0;border-top:5px solid transparent;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid transparent}div.jQBracket .bubble:after{left:-5px;border-left:0}div.jQBracket .bubble.third:after{border-right:6px solid #963}div.jQBracket .bubble.fourth:after{border-right:6px solid #678}div.jQBracket .highlightWinner .bubble{background-color:#da0;color:#960}div.jQBracket .highlightWinner .bubble:after{border-right-color:#da0}div.jQBracket .highlightLoser .bubble{background-color:#ccc;color:#333}div.jQBracket .highlightLoser .bubble:after{border-right-color:#ccc}div.jQBracket.rl .finals{float:left;left:0;clear:left}div.jQBracket.rl .bracket{float:left;clear:right}div.jQBracket.rl .loserBracket{float:left;clear:right}div.jQBracket.rl .round{margin-right:0;margin-left:40px;float:right}div.jQBracket.rl .team{float:right}div.jQBracket.rl .team div.label{right:0}div.jQBracket.rl .team div.score{float:left}div.jQBracket.rl .teamContainer{float:right}div.jQBracket.rl .connector{border-left-style:solid;border-right-style:none;border-width:2px}div.jQBracket.rl .connector.highlightWinner,div.jQBracket.rl .connector div.connector.highlightWinner{border-color:#da0}div.jQBracket.rl .connector.highlightLoser,div.jQBracket.rl .connector div.connector.highlightLoser{border-color:#ccc}div.jQBracket.rl .connector.highlight,div.jQBracket.rl .connector div.connector.highlight{border-color:#0c0}div.jQBracket.rl .bubble{left:-35px}div.jQBracket.rl .bubble.third{background-color:#963;color:#310}div.jQBracket.rl .bubble.fourth{background-color:#678;color:#ccd}div.jQBracket.rl .bubble:after{left:auto;right:-5px;border-left:5px solid transparent;border-right:0}div.jQBracket.rl .bubble.third:after{border-right:0;border-left:6px solid #963}div.jQBracket.rl .bubble.fourth:after{border-right:0;border-left:6px solid #678}div.jQBracket.rl .highlightWinner .bubble:after{border-left-color:#da0}div.jQBracket.rl .highlightLoser .bubble:after{border-left-color:#ccc} 3 | -------------------------------------------------------------------------------- /client/css/vs.css: -------------------------------------------------------------------------------- 1 | .vs-playground { 2 | height: 100%; 3 | float: left; 4 | width: calc(100% - 221px); 5 | left: 1px; 6 | position: relative; 7 | } 8 | 9 | .select-maps { 10 | width: 220px; 11 | float: left; 12 | height: 100%; 13 | border-right: 1px solid rgba(255, 255, 255, 0.1); 14 | } 15 | 16 | .select-maps ul { 17 | padding: 0; 18 | margin: 0; 19 | height: 100%; 20 | overflow-y: auto; 21 | } 22 | 23 | .select-maps li { 24 | padding: 10px; 25 | height: 180px; 26 | list-style: none; 27 | cursor: pointer; 28 | -webkit-transition: 0.2s; 29 | transition: 0.2s; 30 | margin-bottom: 10px; 31 | } 32 | 33 | .select-maps li:hover, 34 | .select-maps li.is-selected { 35 | background: rgba(255, 255, 255, 0.1); 36 | } 37 | 38 | p.hint { 39 | font-size: 60px; 40 | color: #666; 41 | text-align: center; 42 | position: absolute; 43 | line-height: 60px; 44 | margin-top: -30px; 45 | top: 50%; 46 | width: 100%; 47 | } 48 | 49 | .js-playground-layout { 50 | /* height: calc(100% - 57px); */ 51 | height: 100%; 52 | } 53 | 54 | .share { 55 | position: absolute; 56 | right: 10px; 57 | top: 10px; 58 | width: 32px; 59 | } 60 | 61 | .share a { 62 | display: block; 63 | margin-bottom: 5px; 64 | } 65 | -------------------------------------------------------------------------------- /client/images/app.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/app.sketch -------------------------------------------------------------------------------- /client/images/bullet1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/bullet1.png -------------------------------------------------------------------------------- /client/images/bullet2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/bullet2.png -------------------------------------------------------------------------------- /client/images/crash1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/crash1.png -------------------------------------------------------------------------------- /client/images/crash2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/crash2.png -------------------------------------------------------------------------------- /client/images/crash3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/crash3.png -------------------------------------------------------------------------------- /client/images/doc/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/doc/map.png -------------------------------------------------------------------------------- /client/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/favicon.ico -------------------------------------------------------------------------------- /client/images/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/grass.png -------------------------------------------------------------------------------- /client/images/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/ground.png -------------------------------------------------------------------------------- /client/images/header-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/header-logo.png -------------------------------------------------------------------------------- /client/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/logo.png -------------------------------------------------------------------------------- /client/images/share/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/share/twitter.png -------------------------------------------------------------------------------- /client/images/share/weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/share/weibo.png -------------------------------------------------------------------------------- /client/images/smoke1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/smoke1.png -------------------------------------------------------------------------------- /client/images/smoke2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/smoke2.png -------------------------------------------------------------------------------- /client/images/smoke3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/smoke3.png -------------------------------------------------------------------------------- /client/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/star.png -------------------------------------------------------------------------------- /client/images/star_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/star_small.png -------------------------------------------------------------------------------- /client/images/start-btn-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/start-btn-hover.png -------------------------------------------------------------------------------- /client/images/start-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/start-btn.png -------------------------------------------------------------------------------- /client/images/stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/stone.png -------------------------------------------------------------------------------- /client/images/tank1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/tank1.png -------------------------------------------------------------------------------- /client/images/tank1_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/tank1_small.png -------------------------------------------------------------------------------- /client/images/tank2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/tank2.png -------------------------------------------------------------------------------- /client/images/tank2_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/CodeGame/0d0e5dc25c603eb530ec8d3fd8bdf0cd11cf1a41/client/images/tank2_small.png -------------------------------------------------------------------------------- /client/js/app.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var Game = require('./game'); 3 | -------------------------------------------------------------------------------- /client/js/editor.js: -------------------------------------------------------------------------------- 1 | var CodeMirror = require('codemirror'); 2 | require('../../node_modules/codemirror/mode/javascript/javascript.js'); 3 | require('../../node_modules/codemirror/addon/edit/closebrackets.js'); 4 | require('../../node_modules/codemirror/addon/edit/matchbrackets.js'); 5 | require('../../node_modules/codemirror/addon/selection/active-line.js'); 6 | require('../../node_modules/codemirror/addon/lint/lint.js'); 7 | require('../../node_modules/codemirror/addon/lint/javascript-lint.js'); 8 | var jshint = require('jshint'); 9 | var jsonpack = require('jsonpack'); 10 | 11 | var $ = require('jquery'); 12 | 13 | var template = require('./editor/template'); 14 | 15 | var editor = CodeMirror(document.getElementById('editor'), { 16 | value: (typeof existedCode !== 'string') ? template : existedCode, 17 | mode: 'javascript', 18 | theme: 'ambiance', 19 | lineNumbers: true, 20 | lint: { 21 | options: { 22 | asi: true, 23 | undef: true, 24 | browser: false, 25 | globals: ['debug'] 26 | } 27 | }, 28 | styleActiveLine: true, 29 | gutters: ['CodeMirror-lint-markers'], 30 | autoCloseBrackets: true, 31 | matchBrackets: true 32 | }); 33 | 34 | editor.on('change', function() { 35 | $('.js-publish').removeClass('is-disabled'); 36 | }); 37 | 38 | window.onbeforeunload = confirmExit; 39 | 40 | function confirmExit() { 41 | if (!$('.js-publish').hasClass('is-disabled')) { 42 | return '对代码的修改还没有保存,是否要离开该页面?'; 43 | } 44 | } 45 | 46 | var Game = require('./game'); 47 | var game; 48 | $('.js-preview').click(function() { 49 | if ($(this).hasClass('is-disabled')) { 50 | return; 51 | } 52 | $(this).addClass('is-disabled'); 53 | if (game) { 54 | game.stop = true; 55 | } 56 | var code = editor.getValue(); 57 | var enemy = $('.js-enemy').val(); 58 | var map = $('.js-map').val(); 59 | var _this = this; 60 | $.post('/code/preview', { code: code, map: map, enemy: enemy }, function(data) { 61 | var interval = 300 / parseFloat($('.js-speed').val(), 10); 62 | game = new Game(data.map, jsonpack.unpack(data.result), data.names, interval, $('#playground')); 63 | $('.js-playground').css({ visibility: 'visible' }); 64 | $(_this).removeClass('is-disabled'); 65 | }).fail(function(res, _, err) { 66 | if (res.responseJSON && res.responseJSON.err) { 67 | alert(res.responseJSON.err); 68 | } else { 69 | alert(err); 70 | } 71 | $(_this).removeClass('is-disabled'); 72 | }); 73 | }); 74 | 75 | $('.js-publish').click(function() { 76 | if ($(this).hasClass('is-disabled')) { 77 | return; 78 | } 79 | var code = editor.getValue(); 80 | $('.js-publish').addClass('is-disabled'); 81 | $.post('/code', { code: code }, function(data) { 82 | alert('保存成功!'); 83 | }); 84 | }); 85 | 86 | $('.js-close-playground').click(function() { 87 | $('.js-playground').css({ visibility: 'hidden' }); 88 | }); 89 | -------------------------------------------------------------------------------- /client/js/editor/template.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'var lastPosition = null', 3 | 'function onIdle(me, enemy, game) {', 4 | ' if (lastPosition !== null &&', 5 | ' me.tank.position[0] === lastPosition[0] &&', 6 | ' me.tank.position[1] === lastPosition[1]) {', 7 | ' me.turn("left")', 8 | ' }', 9 | ' lastPosition = me.tank.position.slice()', 10 | ' me.go()', 11 | '}' 12 | ].join('\r\n'); 13 | -------------------------------------------------------------------------------- /client/js/game/editor.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | require('jquery.transit'); 3 | 4 | var DIRECTIONS = ['up', 'right', 'down', 'left']; 5 | var DIRECTION_TO_MAP_CODE = { 6 | "up": "a", 7 | "right": "b", 8 | "down": "c", 9 | "left": "d" 10 | } 11 | 12 | function playerMapCode(playerId,direction) { 13 | var code = DIRECTION_TO_MAP_CODE[direction]; 14 | if(playerId == 1) { 15 | return code.toUpperCase(); 16 | } else { 17 | return code; 18 | } 19 | } 20 | 21 | var MapEditor = module.exports = function($dom) { 22 | this.$dom = $dom; 23 | }; 24 | 25 | MapEditor.toggleCycle = { 26 | 'o': '.', 27 | 'x': 'o', 28 | '.': 'x' 29 | }; 30 | 31 | MapEditor.playerToggleCycle = { 32 | null: 'up', 33 | 'up': 'right', 34 | 'right': "down", 35 | "down": 'left', 36 | "left": null 37 | }; 38 | 39 | // @class Tile Represents an editable tile. 40 | // 41 | // @prop $playground 42 | // @prop type . | x | o 43 | // @prop $tile DOM that displays terrain 44 | function Tile($playground,x,y,type) { 45 | this.type = type; 46 | this.x = x; 47 | this.y = y; 48 | this.$playground = $playground; 49 | 50 | this.refreshTile(); 51 | } 52 | 53 | Tile.prototype = { 54 | isGround: function() { 55 | return this.type == "."; 56 | }, 57 | 58 | isPlayer: function() { 59 | return ['a','b','c','d','A','B','C','D'].indexOf(this.type) != -1; 60 | }, 61 | 62 | clear: function() { 63 | this.setType('.'); 64 | }, 65 | 66 | setType: function(type) { 67 | this.type = type; 68 | this.refreshTile(); 69 | }, 70 | 71 | refreshTile: function() { 72 | var $tile = this.$tile; 73 | 74 | if($tile) { 75 | $tile.detach(); 76 | } 77 | 78 | this.$tile = this.makeTileDom(); 79 | 80 | if(this.$tile) { 81 | this.$playground.append(this.$tile) 82 | } 83 | }, 84 | 85 | playerId: function() { 86 | if(~['A','B','C','D'].indexOf(this.type)) { 87 | return 1; 88 | } else if((~['a','b','c','d'].indexOf(this.type))) { 89 | return 0; 90 | } else { 91 | return null; 92 | } 93 | }, 94 | 95 | playerDirection: function() { 96 | var i = ['a','b','c','d'].indexOf(this.type.toLowerCase()); 97 | return DIRECTIONS[i]; 98 | }, 99 | 100 | setPlayer: function(playerId,direction) { 101 | var type = playerMapCode(playerId,direction); 102 | this.setType(type); 103 | }, 104 | 105 | makeTileDom: function() { 106 | var $tile; 107 | var x = this.x; 108 | var y = this.y; 109 | switch(this.type) { 110 | case '.': 111 | return null; 112 | case 'x': 113 | $tile = $("
").css({ 114 | y: y * 50, 115 | x: x * 50 116 | }); 117 | break; 118 | case 'o': 119 | $tile = $("").css({ 120 | top: y * 50, 121 | left: x * 50 122 | }); 123 | break; 124 | default: 125 | if(!this.isPlayer()) { 126 | return null; 127 | } 128 | 129 | $tile = $("").css({ 130 | y: y * 50, 131 | x: x * 50 132 | }); 133 | 134 | if(this.playerId() == 1) { 135 | $tile.addClass("player2"); 136 | } 137 | 138 | turn($tile,this.playerDirection()); 139 | 140 | break; 141 | } 142 | return $tile; 143 | } 144 | }; 145 | 146 | MapEditor.prototype = { 147 | adjustSize: function(w,h) { 148 | this.$playground.detach(); 149 | this.edit(MapEditor.empty(w,h)); 150 | }, 151 | 152 | reset: function() { 153 | this.adjustSize(this.w,this.h); 154 | }, 155 | 156 | edit: function(mapData) { 157 | this.mapData = mapData; 158 | 159 | var w = this.w = mapData.map.length; 160 | var h = this.h = mapData.map[0].length; 161 | 162 | // nested array to track the tiles being edited. 163 | var map = this.map = []; 164 | 165 | this.$dom.css({ 166 | height: h * 50, 167 | width: w * 50 168 | }); 169 | 170 | this.$playground = $("").css({ 171 | height: "100%", width: "100%" 172 | }).click(this.handleClick.bind(this)); 173 | this.$dom.append(this.$playground); 174 | 175 | // track directions of player spawn points 176 | this.players = [null,null]; 177 | 178 | this.render(); 179 | }, 180 | 181 | handleClick: function(e) { 182 | // translate click position to matrix coordinates. 183 | var pos = this.$playground.offset(); 184 | var cx = e.pageX - pos.left; 185 | var cy = e.pageY - pos.top; 186 | var x = Math.floor(cx/50); 187 | var y = Math.floor(cy/50); 188 | if(e.metaKey) { 189 | this.togglePlayer(x,y); 190 | } else { 191 | this.toggleTile(x,y); 192 | } 193 | }, 194 | 195 | // if empty ground: try to create a player. 196 | // if a player: rotate it. 197 | // else nothing 198 | togglePlayer: function(x,y) { 199 | var tile = this.map[x][y]; 200 | 201 | if(tile.isGround()) { 202 | var playerId = this.players.indexOf(null); 203 | 204 | // Conditions to avoid creating new players. 205 | if(playerId == -1) { 206 | // Both players are placed; 207 | return; 208 | } 209 | 210 | var newDirection = this.players[playerId] = "up"; 211 | tile.setPlayer(playerId,newDirection); 212 | 213 | } else if(tile.isPlayer()) { 214 | var playerId = tile.playerId(); 215 | var direction = this.players[playerId]; 216 | var newDirection = this.players[playerId] = MapEditor.playerToggleCycle[direction]; 217 | if(newDirection == null) { 218 | // End of cycle. Remove player 219 | tile.clear(); 220 | } else { 221 | tile.setPlayer(playerId,newDirection); 222 | } 223 | } 224 | }, 225 | 226 | toggleTile: function(x,y) { 227 | var tile = this.map[x][y]; 228 | if(tile.isPlayer()) { 229 | return; 230 | } 231 | var curTileType = tile.type; 232 | var nextTileType = MapEditor.toggleCycle[curTileType]; 233 | tile.setType(nextTileType); 234 | }, 235 | 236 | render: function() { 237 | // render tiles 238 | var data = this.mapData.map; 239 | for (var x = 0; x < data.length; x++) { 240 | if(!this.map[x]) { 241 | this.map[x] = []; 242 | } 243 | for (var y = 0; y < data[0].length; y++) { 244 | var tile = this.map[x][y] = new Tile(this.$playground,x,y,data[x][y]); 245 | if(tile.isPlayer()) { 246 | this.players[tile.playerId()] = tile.playerDirection(); 247 | } 248 | } 249 | } 250 | 251 | var players = this.mapData.players; 252 | for(var i=0; i < players.length; i++) { 253 | var xy = players[i].position; 254 | var direction = this.players[i] = players[i].direction; 255 | var x = xy[0]; 256 | var y = xy[1]; 257 | var tile = this.map[x][y]; 258 | tile.setPlayer(i,direction); 259 | } 260 | }, 261 | 262 | // Pack the map as string. 263 | toMapDataString: function() { 264 | var lines = []; 265 | 266 | for (var y = 0; y < this.map[0].length; y++) { 267 | var line = []; 268 | for (var x = 0; x < this.map.length; x++) { 269 | var tile = this.map[x][y]; 270 | line.push(tile.type); 271 | } 272 | lines.push(line.join("")); 273 | } 274 | return lines.join("|"); 275 | } 276 | }; 277 | 278 | MapEditor.empty = function (w,h) { 279 | var map = []; 280 | for(var i = 0; i < w; i++) { 281 | var col = []; 282 | map.push(col); 283 | for(var j = 0; j < h; j++) { 284 | if(i == 0 || i == w-1 || j == 0 || j == h-1) { 285 | col.push('x'); 286 | } else { 287 | col.push('.'); 288 | } 289 | } 290 | } 291 | return { 292 | players: [], 293 | map: map 294 | }; 295 | } 296 | 297 | function turn($element, direction) { 298 | switch (direction) { 299 | case 'right': 300 | $element.css({ 301 | rotate: '90deg' 302 | }); 303 | break; 304 | case 'down': 305 | $element.css({ 306 | rotate: '180deg' 307 | }); 308 | break; 309 | case 'left': 310 | $element.css({ 311 | rotate: '-90deg' 312 | }); 313 | break; 314 | case 'up': 315 | $element.css({ 316 | rotate: '0deg' 317 | }); 318 | break; 319 | } 320 | } -------------------------------------------------------------------------------- /client/js/game/index.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | require('jquery.transit'); 3 | 4 | $.cssEase._default = $.cssEase.linear = 'linear'; 5 | 6 | var recordsParser = require('./records-parser'); 7 | 8 | function turn($element, direction) { 9 | switch (direction) { 10 | case 'right': 11 | $element.css({ 12 | rotate: '90deg' 13 | }); 14 | break; 15 | case 'down': 16 | $element.css({ 17 | rotate: '180deg' 18 | }); 19 | break; 20 | case 'left': 21 | $element.css({ 22 | rotate: '-90deg' 23 | }); 24 | break; 25 | case 'up': 26 | $element.css({ 27 | rotate: '0deg' 28 | }); 29 | break; 30 | } 31 | } 32 | 33 | var Game = module.exports = function(map, replay, names, interval, playground, consoleDOM) { 34 | if (typeof playground === 'string') { 35 | this.$playground = $($playground); 36 | } else { 37 | this.$playground = playground; 38 | } 39 | if (typeof consoleDOM === 'string') { 40 | this.$consoleDOM = $($consoleDOM); 41 | } else { 42 | this.$consoleDOM = consoleDOM; 43 | } 44 | this.originalReplay = JSON.parse(JSON.stringify(replay)); 45 | this.meta = replay.meta; 46 | 47 | var _this = this; 48 | this.logs = {}; 49 | this.map = map; 50 | this.names = names; 51 | this.meta.players.forEach(function(player, index) { 52 | if (player.logs) { 53 | player.logs.forEach(function(log) { 54 | if (!_this.logs[log.frame]) { 55 | _this.logs[log.frame] = []; 56 | } 57 | _this.logs[log.frame].push({ 58 | player: index, 59 | type: log.type, 60 | frame: log.frame, 61 | escaped: log.escaped, 62 | runTime: log.runTime, 63 | data: log.data 64 | }); 65 | }); 66 | } 67 | }); 68 | this.replay = recordsParser(replay.records); 69 | 70 | this.$playground.empty().addClass('playground').css({ 71 | width: this.map.length * 50, 72 | height: this.map[0].length * 50 + 60 73 | }); 74 | 75 | this.layout(); 76 | this.setInterval(function() { 77 | _this.layout(); 78 | }, 500); 79 | 80 | this.frame = 0; 81 | this.stop = false; 82 | 83 | this._initMap(); 84 | 85 | this.interval = interval; 86 | setTimeout(function() { 87 | _this.play(_this.interval); 88 | }); 89 | }; 90 | 91 | Game.prototype.setTimeout = function(func, interval) { 92 | var _this = this; 93 | setTimeout(function() { 94 | if (!_this.stop) { 95 | func(); 96 | } 97 | }, interval); 98 | }; 99 | 100 | Game.prototype.setInterval = function(func, interval) { 101 | var _this = this; 102 | setTimeout(function() { 103 | if (!_this.stop) { 104 | func(); 105 | _this.setInterval(func, interval); 106 | } 107 | }, interval); 108 | }; 109 | 110 | Game.prototype.layout = function() { 111 | var parent = this.$playground.parent(); 112 | var scaleX = parent.width() / this.$playground.width(); 113 | var scaleY = parent.height() / this.$playground.height(); 114 | var scale = Math.min(scaleX, scaleY); 115 | if (!this.scale) { 116 | this.scale = 1; 117 | } 118 | if (Math.abs(scale - this.scale) > 0.001) { 119 | this.scale = scale; 120 | this.$playground.css({ 121 | transform: 'scale(' + scale + ')' 122 | }); 123 | } 124 | }; 125 | 126 | Game.prototype.print = function(log) { 127 | if (this.$consoleDOM) { 128 | var $logs = this.$consoleDOM.find('.logs'); 129 | var data = log.data; 130 | $logs.append('帧数' + log.frame +
132 | '代码时间' + log.runTime +
133 | 'ms
' + data + ''); 134 | 135 | this.setTimeout(function() { 136 | $logs.scrollTop($logs[0].scrollHeight); 137 | }, 0); 138 | } 139 | if (typeof console[log.type] === 'function') { 140 | console[log.type]('玩家:', log.player, '[帧数:', log.frame, '执行时间:', log.runTime + 'ms]', JSON.parse(log.data)); 141 | } else { 142 | console.log(log); 143 | } 144 | }; 145 | 146 | Game.prototype._initMap = function() { 147 | var x, y, i; 148 | 149 | // Create built-in elements 150 | this.$star = $('').hide(); 151 | this.$playground.append(this.$star); 152 | 153 | this.$bullets = [ 154 | $('').hide(), 155 | $('').hide() 156 | ]; 157 | this.$playground.append(this.$bullets); 158 | 159 | this.$crashs = [ 160 | $('').hide(), 161 | $('').hide() 162 | ]; 163 | this.$playground.append(this.$crashs); 164 | 165 | // Create elements 166 | var tiles = []; 167 | // Init map 168 | for (x = 0; x < this.map.length; ++x) { 169 | for (y = 0; y < this.map[0].length; ++y) { 170 | switch (this.map[x][y]) { 171 | case 'x': 172 | tiles.push($('').css({ 173 | x: x * 50, 174 | y: y * 50 175 | })); 176 | break; 177 | case 'o': 178 | tiles.push($('').css({ 179 | left: x * 50, 180 | top: y * 50 181 | })); 182 | break; 183 | } 184 | } 185 | } 186 | 187 | this.object = {}; 188 | for (i = 0; i < this.meta.players.length; ++i) { 189 | var player = this.meta.players[i]; 190 | var $player = $('').css({ 191 | x: player.tank.position[0] * 50, 192 | y: player.tank.position[1] * 50 193 | }); 194 | 195 | player.tank.player = i; 196 | player.tank.$element = $player; 197 | player.tank.$bullet = this.$bullets[i]; 198 | this.object[player.tank.id] = player.tank; 199 | 200 | turn($player, player.tank.direction); 201 | tiles.push($player); 202 | } 203 | 204 | this.$playground.append(tiles); 205 | 206 | // Init status bar 207 | this.status = { $bar: $('').appendTo(this.$playground) }; 208 | var barHTML = '
胜利者
'); 232 | this.modal.content = {}; 233 | this.modal.content.$winnerImg = $('').appendTo($content); 234 | this.modal.content.$winner = $('').appendTo($content); 235 | $content.append('最终胜利原因
'); 236 | this.modal.content.$reason = $('').appendTo($content); 237 | $('').appendTo($content).click(function() { 238 | _this.stop = true; 239 | setTimeout(function() { 240 | new Game(_this.map, _this.originalReplay, _this.names, _this.interval, _this.$playground, _this.$consoleDOM); 241 | }, 100); 242 | }); 243 | }; 244 | 245 | Game.prototype.play = function(interval) { 246 | if (typeof interval !== 'undefined') { 247 | this.interval = interval; 248 | } 249 | console.log('> ' + this.names[0] + ' 对战 ' + this.names[1] + ' 开始'); 250 | this._onFrame(); 251 | }; 252 | 253 | Game.prototype._onFrame = function() { 254 | if (this.logs[this.frame]) { 255 | this.logs[this.frame].forEach(this.print.bind(this)); 256 | } 257 | this.status.$frames.html(++this.frame); 258 | if (this.replay.length === 0) { 259 | this.modal.$window.fadeIn('fast'); 260 | this.modal.content.$winner.html(this.names[this.meta.result.winner]); 261 | this.modal.content.$winnerImg.addClass('winner-img' + this.meta.result.winner); 262 | var reasonMap = { 263 | crashed: '命中对手', 264 | timeout: '对手代码超时', 265 | star: '吃到更多的星星', 266 | runTime: '代码运行时间更短', 267 | error: '对手代码出错' 268 | }; 269 | this.modal.content.$reason.html(reasonMap[this.meta.result.reason]); 270 | return; 271 | } 272 | var _this = this; 273 | _this.setTimeout(function() { 274 | _this._onFrame(); 275 | }, this.interval); 276 | this.replay.shift().forEach(function(action) { 277 | switch (action.type) { 278 | case 'tank': 279 | var $tank = _this.object[action.objectId].$element; 280 | switch (action.action) { 281 | case 'go': 282 | $tank.transition({ 283 | x: action.position[0] * 50, 284 | y: action.position[1] * 50 285 | }, _this.interval * action.frame); 286 | break; 287 | case 'turn': 288 | switch (action.direction) { 289 | case 'right': 290 | $tank.transition({ 291 | rotate: '+=90deg' 292 | }, _this.interval); 293 | break; 294 | case 'left': 295 | $tank.transition({ 296 | rotate: '-=90deg' 297 | }, _this.interval); 298 | break; 299 | } 300 | break; 301 | case 'crashed': 302 | _this.setTimeout(function() { 303 | _this.$crashs[action.index].show().css({ 304 | left: $tank.css('x'), 305 | top: $tank.css('y') 306 | }).addClass('play'); 307 | $tank.addClass('crashed'); 308 | }, _this.interval); 309 | break; 310 | } 311 | break; 312 | case 'star': 313 | switch (action.action) { 314 | case 'created': 315 | _this.$star.css({ 316 | left: action.position[0] * 50, 317 | top: action.position[1] * 50 318 | }).show(); 319 | break; 320 | case 'collected': 321 | _this.$star.hide(); 322 | var $starCount = _this.status.players[action.by].$starCount; 323 | $starCount.html(parseInt($starCount.html(), 10) + 1); 324 | break; 325 | } 326 | break; 327 | case 'bullet': 328 | var $bullet = _this.object[action.tank.id].$bullet; 329 | switch (action.action) { 330 | case 'created': 331 | $bullet.css({ 332 | x: action.tank.position[0] * 50, 333 | y: action.tank.position[1] * 50 334 | }).removeClass('crashed').show(); 335 | turn($bullet, action.tank.direction); 336 | break; 337 | case 'go': 338 | $bullet.transition({ 339 | x: action.position[0] * 50, 340 | y: action.position[1] * 50 341 | }, _this.interval * action.frame); 342 | break; 343 | case 'crashed': 344 | $bullet.addClass('crashed'); 345 | break; 346 | } 347 | break; 348 | } 349 | }); 350 | }; 351 | -------------------------------------------------------------------------------- /client/js/game/map.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | var Map = module.exports = function(map, callback) { 4 | this.id = map.id; 5 | this.name = map.name; 6 | this.data = map.data.map; 7 | this.callback = callback; 8 | 9 | this.size = { 10 | width: this.data.length * 50, 11 | height: this.data[0].length * 50 12 | }; 13 | }; 14 | 15 | Map.prototype.render = function($dom, width) { 16 | var _this = this; 17 | var $container = $('').css({ 18 | position: 'relative', 19 | width: this.size.width, 20 | height: this.size.height + 100 21 | }).click(function() { 22 | _this.callback.apply(this, arguments); 23 | }); 24 | var $playground = $('').css(this.size).appendTo($container); 25 | var $name = $('').html(this.name).css({ 26 | position: 'absolute', 27 | bottom: 0, 28 | left: 0, 29 | color: '#fff', 30 | width: '100%', 31 | height: '100px', 32 | fontSize: '60px', 33 | lineHeight: '100px', 34 | textAlign: 'center' 35 | }).appendTo($container); 36 | var tiles = []; 37 | // Init map 38 | for (x = 0; x < this.data.length; ++x) { 39 | for (y = 0; y < this.data[0].length; ++y) { 40 | switch (this.data[x][y]) { 41 | case 'x': 42 | tiles.push($('').css({ 43 | x: x * 50, 44 | y: y * 50 45 | })); 46 | break; 47 | case 'o': 48 | tiles.push($('').css({ 49 | left: x * 50, 50 | top: y * 50 51 | })); 52 | break; 53 | } 54 | } 55 | } 56 | $playground.append(tiles); 57 | var scale = width / this.size.width; 58 | $container.css({ 59 | transform: 'scale(' + scale + ')', 60 | transformOrigin: '0 0' 61 | }); 62 | $dom.append($container); 63 | }; 64 | -------------------------------------------------------------------------------- /client/js/game/records-parser.js: -------------------------------------------------------------------------------- 1 | module.exports = function(records) { 2 | var i, j, k, actions, action; 3 | 4 | for (i = records.length - 1; i >= 1; --i) { 5 | actions = records[i]; 6 | for (j = actions.length - 1; j >= 0; --j) { 7 | action = actions[j]; 8 | if (action.type === 'bullet' && action.action === 'go') { 9 | if (action.order === 1) { 10 | actions[j - 1].position = action.position; 11 | actions[j - 1].frame = 1; 12 | actions.splice(j, 1); 13 | } 14 | if (action.order === 0 && !action.frame) { 15 | action.frame = 0.5; 16 | } 17 | } 18 | } 19 | } 20 | 21 | for (i = records.length - 1; i >= 1; --i) { 22 | actions = records[i]; 23 | for (j = actions.length - 1; j >= 0; --j) { 24 | action = actions[j]; 25 | if (action.action === 'go') { 26 | if (!action.frame) { action.frame = 1; } 27 | var found = false; 28 | for (k = records[i - 1].length - 1; k >= 0; --k) { 29 | var prevActions = records[i - 1][k]; 30 | if (prevActions.type === action.type && 31 | prevActions.action === 'go' && 32 | prevActions.objectId === action.objectId) { 33 | prevActions.frame = action.frame + (prevActions.frame || 1); 34 | prevActions.position = action.position; 35 | found = true; 36 | break; 37 | } 38 | } 39 | if (found) { 40 | actions.splice(j, 1); 41 | } 42 | } 43 | } 44 | } 45 | 46 | return records; 47 | }; 48 | -------------------------------------------------------------------------------- /client/js/history.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var Game = require('./game'); 3 | var jsonpack = require('jsonpack'); 4 | 5 | $(function() { 6 | var url = location.href; 7 | var title = document.title; 8 | $('.js-weibo').attr('href', 'http://v.t.sina.com.cn/share/share.php?url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent(title)); 9 | $('.js-twitter').attr('href', 'http://twitter.com/home/?status=' + encodeURIComponent(title) + ':' + url); 10 | new Game(map, jsonpack.unpack(result), users, 300, $('#playground')); 11 | }); 12 | -------------------------------------------------------------------------------- /client/js/map-editor.js: -------------------------------------------------------------------------------- 1 | // @global mapData; // from template 2 | 3 | var $ = require('jquery'); 4 | var MapEditor = require('./game/editor'); 5 | 6 | $(function() { 7 | var $editor = $("#map-editor"); 8 | var size = getMapSize(); 9 | 10 | if(mapData == null) { 11 | mapData = MapEditor.empty(10,10); 12 | } 13 | 14 | editor = new MapEditor($editor); 15 | editor.edit(mapData); 16 | 17 | var $controls = $("#map-editor-controls"); 18 | $("input[name=width]").val(mapData.map.length); 19 | $("input[name=height]").val(mapData.map[0].length); 20 | 21 | $controls.find("input[name=width], input[name=height]").change(function (e) { 22 | resizeMapEditor(); 23 | }); 24 | 25 | $controls.find("input.reset").click(function() { 26 | editor.reset(); 27 | }); 28 | 29 | $("form").submit(function() { 30 | var mapDataString = editor.toMapDataString() 31 | $("form input[name=data]").val(mapDataString); 32 | return true; 33 | }); 34 | 35 | function getMapSize() { 36 | var w = $("input[name=width]").val(); 37 | var h = $("input[name=height]").val(); 38 | return [w,h]; 39 | } 40 | 41 | function resizeMapEditor() { 42 | var size = getMapSize(); 43 | editor.adjustSize(size[0],size[1]); 44 | } 45 | }); -------------------------------------------------------------------------------- /client/js/tournament.js: -------------------------------------------------------------------------------- 1 | var $ = window.jQuery = require('jquery'); 2 | require('./vendor/bracket.js'); 3 | $(function() { 4 | if (typeof result === 'undefined') { 5 | return; 6 | } 7 | var bracket = {}; 8 | bracket.teams = result.teams.map(function(round) { 9 | return round.map(function(user) { 10 | return user.name || user; 11 | }); 12 | }); 13 | bracket.results = result.results; 14 | $('#tournamentResult').bracket({ 15 | init: bracket, 16 | onMatchClick: function(data) { 17 | if (!data) { 18 | return; 19 | } 20 | location.href = '/tournaments/' + tournamentId + '/replays/' + data.id; 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/js/vendor/bracket.js: -------------------------------------------------------------------------------- 1 | /* jQuery Bracket | Copyright (c) Teijo Laine 2011-2013 | Licenced under the MIT licence */ 2 | !function(a){function b(a){return!isNaN(parseFloat(a))&&isFinite(a)}function c(a){function b(a,c){return a instanceof Array?b(a[0],c+1):c}return b(a,0)}function d(a,b){return b>0&&(a=d([a],b-1)),a}function e(){return{source:null,name:null,id:-1,idx:-1,score:null}}function f(a){if(b(a.a.score)&&b(a.b.score)){if(a.a.score>a.b.score)return[a.a,a.b];if(a.a.scoreonIdle
函数,在游戏运行过程中,一旦脚本所控制的坦克处于空闲状态,游戏引擎就会调用脚本的 onIdle
函数,并将游戏的状态(如地图信息,坦克、炮弹和星星的位置,当前帧数)传递给该函数。在 onIdle
函数中,开发者需要做的就是根据游戏的状态向自己控制的坦克下达指令(前进、转向和开火)。当坦克执行完所有操作后,游戏引擎会再次请求 onIdle
函数,从而形成循环。
24 | p 游戏引擎会传递给 onIdle
3个参数,分别为 me
, enemy
和 game
,下面分别介绍。
25 |
26 | h3
27 | code me
28 | p me
对象包含己方坦克、炮弹的状态信息,除此之外还可以通过 me
对象向己方坦克下达指令。me
的属性如下表所示。
29 | table
30 | thead
31 | tr
32 | th 属性
33 | th 类型
34 | th 说明
35 | tbody
36 | tr
37 | td
38 | code me.stars
39 | td
40 | code Number
41 | td 己方收集到的星星数量
42 | tr
43 | td
44 | code me.tank
45 | td
46 | code Object
47 | td 己方坦克状态对象
48 | tr
49 | td
50 | code me.tank.position
51 | td
52 | code Array
53 | td 坦克的位置,两个元素,分别为横坐标和纵坐标,从 0 起算。如 [0, 17]
54 | tr
55 | td
56 | code me.tank.direction
57 | td
58 | code String
59 | td 坦克的朝向,可能取值为 "up"
, "down"
, "left"
, "right"
。分别代表坦克朝上、朝下、朝左和朝右。
60 | tr
61 | td
62 | code me.bullet
63 | td
64 | code Object
65 | td 炮弹的状态对象,和 me.tank
格式一样。如果场上不存在炮弹,则为 null
。
66 |
67 | p me
的方法如下表所示。
68 | table
69 | thead
70 | tr
71 | th 方法
72 | th 参数
73 | th 说明
74 | tbody
75 | tr
76 | td
77 | code me.go(steps)
78 | td steps
(Number
) 表示前进步数,默认为 1
79 | td 命令坦克前进指定步数
80 | tr
81 | td
82 | code me.turn(direction)
83 | td direction
(String
) 表示转向,只能为 "left"
或 "right"
84 | td 命令坦克向左(或向右)转弯
85 | tr
86 | td
87 | code me.fire()
88 | td 无
89 | td 命令坦克朝当前方向发射一枚炮弹
90 | p 需要注意的是方法并不是同步执行的(即不是等待坦克执行完指令后再继续运行代码),而是将指令放入一个队列中等待脚本执行结束再执行。当队列有指令,游戏引擎会执行队列中的指令,否则会调用 `onIdle` 函数获取指令。当坦克当前指令无效时,游戏引擎会清空指令队列。
91 |
92 | h3
93 | code enemy
94 | p enemy
对象包含敌方坦克、炮弹的状态信息。其属性和 me
完全相同,但不含有任何方法。当敌方坦克不可见时(在草丛中),enemy.tank
为 null
,当敌方子弹不可见时,enemy.bullet
为 null
。
95 |
96 | h3
97 | code game
98 | p game
对象存储当前游戏状态,属性如下表所示。
99 | table
100 | thead
101 | tr
102 | th 属性
103 | th 类型
104 | th 说明
105 | tbody
106 | tr
107 | td
108 | code game.map
109 | td
110 | code Array
111 | td 游戏地图。二维数组,分别代表横坐标和纵坐标。每个元素均为一个字符,取值有 "."
表示空地、"x"
表示石头、"o"
表示草地。左上角为 [0, 0]
。
112 | tr
113 | td
114 | code game.frames
115 | td
116 | code Number
117 | td 游戏当前帧数
118 | tr
119 | td
120 | code game.star
121 | td
122 | code Array
123 | span 或
124 | code null
125 | td 当前星星位置,如果星星不存在则为 null
126 |
127 | h2 调试
128 | p 游戏引擎提供了全局函数 print()
使得开发者可以方便地进行打印调试,无论是在代码编辑页面还是与他人对战页面,调试信息都可以在浏览器开发者工具的控制台看到。需要注意的是 print
函数要打印的内容必须是可 JSON 化的(无循环引用,非函数)。
129 | h2 接口示例
130 | span 从零开始介绍如何实现一个简单的脚本
131 |
132 | p Code Game 提供了非常友好的接口使得开发一个脚本非常容易,同时 Code Game 还支持脚本调试,登录后可以在编辑脚本页面一边编写脚本一边查看脚本运行结果。下面的内容将一步步地实现一个简易的脚本,借此可以让开发者对脚本开发有个直观的了解。
133 | p 定义 onIdle
函数:
134 | pre
135 | | function onIdle(me, enemy, game) {
136 | | }
137 |
138 | p 我们先实现一个只会令坦克前进的脚本:
139 | pre
140 | | function onIdle(me, enemy, game) {
141 | | me.go()
142 | | }
143 |
144 | p 这时运行发现坦克撞到石头上就停下来了。这是因为坦克前进撞到石头后,已经不能再前进了,所以 go
指令就无效了。
145 | p 下面我们实现让坦克再遇到障碍物就转弯,原理是执行 go
命令后,判断当前位置和上一次的位置是否相同,如果相同则随机转弯:
146 | pre
147 | | var lastPosition = null
148 | | function onIdle(me, enemy, game) {
149 | | if (lastPosition !== null &&
150 | | lastPosition[0] === me.tank.position[0] &&
151 | | lastPosition[1] === me.tank.position[1]) {
152 | | // 坦克没有动,证明遇到障碍物了
153 | | var turn = ['left', 'right'][Math.floor(Math.random() * 2)]
154 | | me.turn(turn)
155 | | } else {
156 | | lastPosition = me.tank.position
157 | | }
158 | | me.go()
159 | | }
160 |
161 | p 现在我们的坦克已经可以自动转弯了。这段代码很好地体现了指令异步按队列执行的特性,`me.go()` 的本质是将 `go` 指令加入的指令队列中,并不是真的移动坦克。所以在 `me.go()` 后面判断位置变没变是没有意义的,也因此我们选择了在 `onIdle` 函数开头判断位置的变化,这时游戏引擎已经执行完了上一次发送的 `go` 命令。另外一点是在转向时连续发了两个指令(即 me.turn()
和 me.go()
),这是游戏引擎会按照先后顺序依次执行两个指令(即转弯后再前进一步)。
162 |
163 | p 现在我们的坦克还不能开火,不是很厉害。下面我们让坦克在遇到对方时(自己与对方横纵坐标有一个是相同的)开火。
164 |
165 | pre
166 | | var lastPosition = null
167 | | function onIdle(me, enemy, game) {
168 | | if (enemy.tank) {
169 | | // 即敌方坦克不在草丛中
170 | | if (!me.bullet) {
171 | | // 因为场上只能存在一枚己方炮弹,所以在这儿判断一下,以免白白浪费指令
172 | | if (me.tank.position[0] === enemy.tank.position[0] ||
173 | | me.tank.position[1] === enemy.tank.position[1]) {
174 | | me.fire()
175 | | }
176 | |
177 | | }
178 | | }
179 | | if (lastPosition !== null &&
180 | | lastPosition[0] === me.tank.position[0] &&
181 | | lastPosition[1] === me.tank.position[1]) {
182 | | // 坦克没有动,证明遇到障碍物了
183 | | var turn = ['left', 'right'][Math.floor(Math.random() * 2)]
184 | | me.turn(turn)
185 | | } else {
186 | | lastPosition = me.tank.position
187 | | }
188 | | me.go()
189 | | }
190 |
191 | p 好了,我们的坦克可以开火了!这里需要注意的是发射炮弹前我们判断了 me.bullet
是否存在,如果不存在才发射。这是因为场上只能存在一枚己方炮弹。如果已经存在一枚,执行 fire
指令时,游戏引擎会自动忽略该指令,导致该帧不会执行任何有效指令。
192 | p 当然这段代码仍有一些问题,最明显的就是即使没有朝向敌方,如果横纵坐标相同也会开炮,同时也没有判断中间是否隔有障碍物等等。另一个问题是现在只有撞墙后才会转向,所以会多出一个无效的 go
指令,以至于每次转向前都会停一帧。这就需要根据地图实现寻路算法了。这些需要改进的地方就交给大家来做了,加油!
193 |
--------------------------------------------------------------------------------
/views/doc/rule.jade:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | block content
4 | include ../include/header.jade
5 | .container
6 | aside
7 | ul
8 | li 游戏介绍
9 | li
10 | a(href="/doc/tournament") 杯赛规则
11 | li
12 | a(href="/doc/api") AI 编写文档
13 | article
14 | h2 概述
15 | span 什么是 Code Game
16 | p Code Game 是一个使用 JavaScript 代码编写 AI 脚本来和他人进行对抗的游戏平台。
17 |
18 | h2 玩法
19 | span 了解 Code Game 的地图、规则和获胜条件
20 | p Code Game 以坦克大战游戏为原型,每局比赛由两名玩家参与,每名玩家以事先编写好的 JavaScript 脚本参与比赛,每名玩家控制一辆坦克。
21 |
22 | h3 地图介绍
23 | p 游戏地图为矩形,如下所示:
24 | img(src="/public/images/doc/map.png" width="634" height="540" alt="游戏地图")
25 | p 其中各个元素的介绍如下:
26 | ul.elements
27 | li
28 | img(src="/public/images/stone.png" width="50" height="50" alt="石头")
29 | p 石头。坦克和炮弹均无法通行,每个游戏地图最外圈都是由石头围成的。
30 | li
31 | img(src="/public/images/grass.png" width="50" height="50" alt="草丛")
32 | p 草丛。坦克和炮弹均可以通行,但处于草丛中的坦克无法被对手发现。
33 | li
34 | img(src="/public/images/tank1.png" width="50" height="50" alt="玩家1")
35 | p 玩家 1 控制的坦克
36 | li
37 | img(src="/public/images/tank2.png" width="50" height="50" alt="玩家2")
38 | p 玩家 2 控制的坦克
39 | li
40 | img(src="/public/images/star.png" width="50" height="50" alt="星星")
41 | p 星星。坦克可以收集星星。
42 |
43 | p
44 | strong 需要注意的是地图的尺寸和各个元素的位置可能会发生变化,未来游戏也会支持多个地图,所以请通过 API 接口获取地图信息,而不要在脚本中硬编码。
45 | h3 游戏规则
46 | p 游戏以“帧”为时间单位,“格”为距离单位。一格的距离是一个元素(即石头、草丛等)的大小,如上面的地图的宽度为 19 格。其中:
47 | ol
48 | li 坦克一帧可以移动一格
49 | li 坦克开火需要一帧时间
50 | li 坦克 90 度原地转弯需要一帧时间
51 | li 炮弹一帧可以移动两格
52 | p 对坦克来说,可以进行的操作为“前进”、“向左转”、“向右转”和“开火”,同一时间内只能进行一个操作(比如开火时需要静止)。
53 | p 星星每次都会出现在两辆坦克的中央位置(即[(x1 + x2) / 2, (y1 + y2) / 2]),场上最多只会存在一个星星。星星第一次会在第一帧出现,当星星被坦克收集到后,10帧后将会再次出现。如果出现时的中央位置无法放置星星(该位置为石头)或者无法计算出中央位置(两辆坦克相邻偶数格,这时计算出的中间位置坐标为小数)时,星星会延至下一帧出现。
54 | p 每个地图坦克出场位置和朝向都是固定的。
55 |
56 | h3 脚本可见规则
57 | p 尽管作为观看者可以看到游戏中各个元素的位置,但是出于竞技性考虑,AI 脚本并非如此,具体规则如下。
58 | ul
59 | li 可以看到敌方坦克的位置和朝向,除非敌方坦克处于草丛中
60 | li 可以看到星星的位置,即使星星处于草丛中
61 | li 己方坦克仅当面向敌方子弹且与敌方子弹处于同一条直线、两者间没有障碍物(石头)时才能看到敌方子弹;己方子弹永久可见。
62 |
63 | h3 胜利条件
64 | p 游戏的胜利条件按优先级排列如下:
65 | ol
66 | li 一方击中另一方坦克时即获胜
67 | li 如果第 128 帧时两辆坦克均存活,游戏即结束,收集星星较多的坦克获胜
68 | li 如果两辆坦克收集的星星数量相同,代码执行时间短的玩家获胜
69 | p 例外的情况是如果一方的代码运行出错或超时,即告负。
70 |
--------------------------------------------------------------------------------
/views/doc/tournament.jade:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | block content
4 | include ../include/header.jade
5 | .container
6 | aside
7 | ul
8 | li
9 | a(href="/doc") 游戏介绍
10 | li 杯赛规则
11 | li
12 | a(href="/doc/api") AI 编写文档
13 | article
14 | h2 杯赛概述
15 | span 什么是杯赛
16 | p 杯赛是独立的赛制,与排行榜不同,每个杯赛都是有开始和结束时间限制的。杯赛开始后,玩家可以在杯赛页面自由报名参赛或退赛。杯赛结束时,CodeGame 会以淘汰赛制决出名次。每个杯赛根据主办方不同,具体的活动细节、奖品等也不尽相同,可以到杯赛页面查看具体情况。杯赛欢迎任何企业、组织甚至个人举办,具体可以联系 codegame@zihua.li。
17 |
18 | h2 赛制
19 | p 赛制为淘汰赛。参加玩家数量为 2 的整数次幂,如果数量不足则以轮空补足,排行榜排名较高的玩家优先轮空。举例来说,共有 3 名玩家参赛,玩家 A 排名第一,玩家 B 排名第 7,玩家 C 排名第 8。则玩家 A 第一轮轮空,并在第二轮对阵 B 和 C 之间的胜者。
20 |
21 | h2 录像
22 | p 淘汰赛的每场比赛的录像都会保存,点击对阵图可以进入录像查看页面。
23 |
24 | h2 重赛
25 | p 当发生下列情况时杯赛将重赛:
26 | ul
27 | li 明显的服务器原因导致选手代码超时
28 | li 杯赛排名计算错误
29 |
--------------------------------------------------------------------------------
/views/editor.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | include ./include/header.jade
5 | .wrap
6 | #editor
7 | div.playground-container.js-playground
8 | .js-close-playground X
9 | #playground
10 | .toolbar
11 | button.button.small-button.is-disabled.publish-btn.js-publish.item 保存
12 | .preview-options
13 | input.js-enemy(type="text", name="enemy", placeholder="对手用户名,留空与相同代码对战")
14 | select.js-speed(name="speed")
15 | option(value="0.5") 0.5倍速
16 | option(value="1", selected="selected") 1倍速
17 | option(value="2") 2倍速
18 | option(value="5") 5倍速
19 | select.js-map(name="map")
20 | each map in maps
21 | option(value=map.id) #{map.name}
22 | button.button.small-button.preview-btn.js-preview 预览
23 | script.
24 | existedCode = !{JSON.stringify(code)}
25 | script(src="/public/js/editor.js")
26 |
--------------------------------------------------------------------------------
/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/views/history.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | include ./include/header.jade
5 | script.
6 | var result = !{JSON.stringify(history.Result.data)}
7 | var users = !{JSON.stringify(users)}
8 | var map = !{JSON.stringify(map.parse().map)}
9 | .wrap.is-dark
10 | div(style="height:100%;")
11 | #playground(style="margin: 0 auto;")
12 | .share
13 | a.js-weibo(target="_blank")
14 | img(src="/public/images/share/weibo.png", width="32", height="32")
15 | a.js-twitter(target="_blank")
16 | img(src="/public/images/share/twitter.png", width="32", height="32")
17 | script(src="/public/js/history.js")
18 |
--------------------------------------------------------------------------------
/views/include/header.jade:
--------------------------------------------------------------------------------
1 | header.top
2 | a(href="/")
3 | img.header-logo(src="/public/images/header-logo.png", width="174", height="25", alt="Code Game")
4 | ul.pull-left
5 | if me
6 | li
7 | a(href="/#{me.login}") 个人主页
8 | else
9 | li
10 | a(href="/") 首页
11 | li
12 | a(href="/rank") 排行榜
13 | li
14 | a(href="/tournaments") 杯赛
15 | li
16 | a(href="/doc") 文档
17 | li
18 | a(href="https://github.com/luin/CodeGame/issues") 讨论
19 | if me && me.isAdmin()
20 | li
21 | a(href="/map") 地图
22 | if me
23 | ul.pull-right
24 | li.ad
25 | a(href="/tournaments/1") 参与 Coding AI 邀请赛拿奖品
26 | li #{me.name}
27 | li
28 | a(href="/account/logout") 退出
29 |
30 | else
31 | ul.pull-right
32 | li.ad
33 | a(href="/tournaments/1") 快来参与 Coding.net AI 邀请赛
34 | li
35 | a(href="/account/github") 通过 GitHub 登录
36 |
--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .index.perspective
5 | .logo
6 | img(src="/public/images/logo.png", width="378", height="47", alt="Code Game")
7 | h2.desc Code GAME 是一个通过编写 AI 脚本控制坦克进行比赛的游戏
8 | a(href="/account/github")
9 | button.start
10 | p 或先
11 | a(href="/doc") 查看规则与开发文档
12 | #preload
13 | img(src="/public/images/start-btn-hover.png")
14 |
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | if locals.title
5 | title #{title}
6 | else
7 | title Code Game
8 | link(rel='stylesheet', href='/public/css/app.css')
9 | link(href='http://fonts.useso.com/css?family=Source+Code+Pro:400,700', rel='stylesheet', type='text/css')
10 | link(rel="shortcut icon", href="/public/images/favicon.ico", type="image/x-icon")
11 | link(rel="icon", href="/public/images/favicon.ico", type="image/x-icon")
12 | body
13 | block content
14 | if config.googleAnalytics
15 | script.
16 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
17 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
18 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
19 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
20 |
21 | ga('create', '#{config.googleAnalytics}', 'auto');
22 | ga('send', 'pageview');
23 |
--------------------------------------------------------------------------------
/views/map/edit.jade:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | block content
4 | include ../include/header.jade
5 | script.
6 | var mapData = !{JSON.stringify(map)}
7 | ul
8 | li cmd-click - Place tank & rotate tank.
9 | li click - Cycle through types of obstacles.
10 |
11 | div#map-editor-controls
12 | p
13 | span width
14 | input(name="width" type="text" value=10)
15 | span height
16 | input(name="height" type="text" value=10)
17 | p
18 | input.reset(type="submit" value="重置")
19 |
20 | #map-editor
21 |
22 | form#new-map(action="/map/update" method="post")
23 | input(name="id", value=mapId)
24 |
25 | p
26 | input(name="data" type="hidden")
27 |
28 | input(type="submit" value="储存")
29 |
30 | script(src="/public/js/map-editor.js")
--------------------------------------------------------------------------------
/views/map/index.jade:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | block content
4 | include ../include/header.jade
5 |
6 | h2 创建新地图
7 |
8 | form#new-map(action="/map/create" method="post")
9 | p
10 | label(for="name") 地图名称
11 | input(name="name")
12 |
13 | p
14 | label(for="type") 地图种类
15 | select(name="type")
16 | option(value="general") 普通
17 | option(value="rank") 排名赛
18 | option(value="tournament") 杯赛
19 |
20 | input(type="submit" value="create")
21 |
22 | h2 所有地图
23 |
24 | ul
25 | each map in maps
26 | li
27 | a(href="/map/" + map.id + "/edit") #{map.id} #{map.name}
--------------------------------------------------------------------------------
/views/rank.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | include ./include/header.jade
5 |
6 | .rank-section
7 | h2 全网排行榜(前 #{rank.length} 名)
8 | span 测试阶段每天至少更新一次
9 | table
10 | thead
11 | tr
12 | th 排名
13 | th 玩家
14 | th 胜
15 | th 负
16 | th 胜率
17 | th 致胜最多的原因
18 | th 失败最多的原因
19 | tbody
20 | each item, index in rank
21 | tr
22 | td #{index + 1}
23 | td
24 | a(href="/#{item.User.login}")
25 | img(src="#{item.User.avatar}", width="32")
26 | if item.User.Tournaments.length > 0
27 | span [杯赛]
28 | td #{item.win}
29 | td #{item.lost}
30 | td #{Math.round(item.win / (item.win + item.lost) * 100)}%
31 | - var winReasonMap = { crashed: '击毁对手', timeout: '对手代码超时', star: '抢星星', runTime: '代码运行时间短', error: '对手代码出错' };
32 | td #{winReasonMap[item.winReason]}
33 | - var lostReasonMap = { crashed: '被击毁', timeout: '代码超时', star: '抢星星', runTime: '运行时间较长', error: '代码出错' };
34 | td #{lostReasonMap[item.loseReason]}
35 |
--------------------------------------------------------------------------------
/views/tournaments/index.jade:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | block content
4 | include ../include/header.jade
5 |
6 | ul.tournaments
7 | each tournament in tournaments
8 | li
9 | a(href="/tournaments/#{tournament.id}") #{tournament.name}
10 |
--------------------------------------------------------------------------------
/views/tournaments/show.jade:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | block content
4 | include ../include/header.jade
5 |
6 | .tournament
7 | script.
8 | var tournamentId = #{tournament.id};
9 | h2.tournament-name #{tournament.name}
10 | .tournament-time 开始时间:#{moment(tournament.start).format('YYYY年MM月DD日 HH:mm')}
11 | .tournament-time 结束时间:#{moment(tournament.end).format('YYYY年MM月DD日 HH:mm')}
12 | .tournament-description !{tournament.description}
13 | - var attended = false
14 | if req.me
15 | each user in tournament.Users
16 | if user.id == req.me.id
17 | - attended = true
18 | if Date.now() < tournament.start
19 | p 比赛还未开始,请在 #{moment(tournament.start).format('YYYY年MM月DD日 HH:mm')} 后加入
20 | else if Date.now() > tournament.end
21 | p 比赛已经结束
22 | else
23 | if req.me
24 | .tournament-action
25 | if attended
26 | form(action="/tournaments/#{tournament.id}/action/leave", method="post")
27 | button.button(type="submit") 退出杯赛
28 | else
29 | form(action="/tournaments/#{tournament.id}/action/join", method="post")
30 | button.button(type="submit") 加入杯赛
31 | h3 参赛选手
32 | ul.tournament-users
33 | each user in tournament.Users
34 | li
35 | a(href="/#{user.login}")
36 | img(src=user.avatar, width="64", height="64")
37 | hr
38 | h3 比赛结果
39 | if tournament.result && (Date.now() > tournament.end || (req.me && req.me.isAdmin()))
40 | script.
41 | var result = !{tournament.result};
42 | #tournamentResult
43 | else
44 | p 战斗尚未打响,预计开战时间为 #{moment(tournament.end).format('YYYY年MM月DD日 HH:mm')} 左右
45 | script(src="/public/js/tournament.js")
46 |
--------------------------------------------------------------------------------
/views/user.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | include ./include/header.jade
5 | .user-info
6 | .avatar
7 | img(src=user.avatar)
8 | h2.name #{user.name}
9 | p.bio #{user.bio}
10 | if code
11 | p 全球排名
12 | if code.rank
13 | .rank #{code.rank}
14 | else
15 | .rank 暂无
16 | if me
17 | if user.id == me.id
18 | a(href="/code/editor")
19 | button.button 编辑我的 AI
20 | else
21 | a(href="/#{user.login}/vs/#{me.login}")
22 | button.button 发起挑战
23 | else
24 | if me
25 | if user.id == me.id
26 | a(href="/code/editor")
27 | button.button 创建我的 AI
28 | else
29 | a(href="/#{user.login}/vs/#{me.login}")
30 | button.button 发起挑战
31 |
--------------------------------------------------------------------------------
/views/vs.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | include ./include/header.jade
5 | script.
6 | var maps = !{JSON.stringify(maps)}
7 | var user1 = !{JSON.stringify(user1)}
8 | var user2 = !{JSON.stringify(user2)}
9 | .wrap.is-dark
10 | .select-maps
11 | ul.js-select-map
12 | .vs-playground
13 | div.js-playground-layout
14 | p.hint 请选择地图
15 | #playground
16 | .share
17 | a.js-weibo(target="_blank")
18 | img(src="/public/images/share/weibo.png", width="32", height="32")
19 | a.js-twitter(target="_blank")
20 | img(src="/public/images/share/twitter.png", width="32", height="32")
21 | script(src="/public/js/vs.js")
22 |
--------------------------------------------------------------------------------