├── .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 = '
0
'; 209 | var starBar = this.status.$bar.append('
' + barHTML + '
' + 210 | '
' + barHTML + '
' + 211 | '
0
'); 212 | this.status.players = this.status.$bar.find('.player-bar').map(function() { 213 | return { 214 | $starCount: $(this).find('.star-count'), 215 | $name: $(this).find('.name') 216 | }; 217 | }).get(); 218 | 219 | var _this = this; 220 | this.names.forEach(function(name, index) { 221 | _this.status.players[index].$name.html(name); 222 | }); 223 | this.status.$frames = this.status.$bar.find('.frames'); 224 | 225 | // Init modal 226 | this.modal = { 227 | $window: 228 | $('
').appendTo(this.$playground).hide() 229 | }; 230 | var $content = $('
').appendTo(this.modal.$window.children('.modal')); 231 | $content.append('

胜利者

'); 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.score');e.val(c),b.html(e),e.focus(),e.blur(function(){d(e.val())}),e.keydown(function(a){var b=a.keyCode||a.which;(9===b||13===b||27===b)&&(a.preventDefault(),d(e.val(),27!==b))})}function l(a,b){a.append(b)}function m(a){var b=a.el,c=b.find(".team.win");c.append('
1st
');var d=b.find(".team.lose");return d.append('
2nd
'),!0}function n(a){var b=a.el,c=b.find(".team.win");c.append('
3rd
');var d=b.find(".team.lose");return d.append('
4th
'),!0}function o(a,b,c,d){for(var e,f=Math.log(2*b.length)/Math.log(2),g=b.length,h=0;f>h;h+=1){e=a.addRound();for(var i=0;g>i;i+=1){var j=null;if(0===h&&(j=function(){var a=b[i],c=i;return[{source:function(){return{name:a[0],idx:2*c}}},{source:function(){return{name:a[1],idx:2*c+1}}}]}),h===f-1&&c){var k=e.addMatch(j,m);k.setAlignCb(function(a){a.css("top",""),a.css("position","absolute"),d?a.css("top",k.el.height()/2-a.height()/2+"px"):a.css("bottom",-a.height()/2+"px")})}else e.addMatch(j)}g/=2}if(c&&(a.final().connectorCb(function(){return null}),b.length>1&&!d)){var l=a.final().round().prev().match(0).loser,o=a.final().round().prev().match(1).loser,p=e.addMatch(function(){return[{source:l},{source:o}]},n);p.setAlignCb(function(b){var c=a.el.height()/2;p.el.css("height",c+"px");var d=b.height();b.css("top",d+"px")}),p.connectorCb(function(){return null})}}function p(a,b,c){for(var d=Math.log(2*c)/Math.log(2)-1,e=c/2,f=0;d>f;f+=1){for(var g=0;2>g;g+=1)for(var h=b.addRound(),i=0;e>i;i+=1){var j=null;(0!==g%2||0===f)&&(j=function(){if(0===g%2&&0===f)return[{source:a.round(0).match(2*i).loser},{source:a.round(0).match(2*i+1).loser}];var c=i;return 0===f%2&&(c=e-i-1),[{source:b.round(2*f).match(i).winner},{source:a.round(f+1).match(c).loser}]});var k=h.addMatch(j),l=k.el.find(".teamContainer");if(k.setAlignCb(function(){l.css("top",k.el.height()/2-l.height()/2+"px")}),d-1>f||1>g){var m=null;0===g%2&&(m=function(a,b){var c=a.height()/4,d=0,e=0;return 0===b.winner().id?e=c:1===b.winner().id?(d=2*-c,e=c):e=2*c,{height:d,shift:e}}),k.connectorCb(m)}}e/=2}}function q(a,b,c,d,e,f){var g=a.addRound(),h=g.addMatch(function(){return[{source:b.winner},{source:c.winner}]},function(e){var g=!1;if(d||null===e.winner().name||e.winner().name!==c.winner().name)return m(e);if(2!==a.size()){var h=a.addRound(function(){var b=null!==e.winner().name&&e.winner().name===c.winner().name;return g===!1&&b&&(g=!0,f.css("width",parseInt(f.css("width"),10)+140+"px")),!b&&g&&(g=!1,a.dropRound(),f.css("width",parseInt(f.css("width"),10)-140+"px")),b}),i=h.addMatch(function(){return[{source:e.first},{source:e.second}]},m);return e.connectorCb(function(a){return{height:0,shift:a.height()/2}}),i.connectorCb(function(){return null}),i.setAlignCb(function(a){var d=b.el.height()+c.el.height();i.el.css("height",d+"px");var e=(b.el.height()/2+b.el.height()+c.el.height()/2)/2-a.height();a.css("top",e+"px")}),!1}});h.setAlignCb(function(a){var d=b.el.height()+c.el.height();e||(d/=2),h.el.css("height",d+"px");var f=(b.el.height()/2+b.el.height()+c.el.height()/2)/2-a.height();a.css("top",f+"px")});var i,j;if(!e){var k=c.final().round().prev().match(0).loser,l=g.addMatch(function(){return[{source:k},{source:c.loser}]},n);l.setAlignCb(function(a){var d=(b.el.height()+c.el.height())/2;l.el.css("height",d+"px");var e=(b.el.height()/2+b.el.height()+c.el.height()/2)/2+a.height()/2-d;a.css("top",e+"px")}),h.connectorCb(function(){return null}),l.connectorCb(function(){return null})}b.final().connectorCb(function(a){var d=a.height()/4,e=(b.el.height()/2+b.el.height()+c.el.height()/2)/2-a.height()/2,f=e-b.el.height()/2;return 0===b.winner().id?(j=f+2*d,i=d):1===b.winner().id?(j=f,i=3*d):(j=f+d,i=2*d),j-=a.height()/2,{height:j,shift:i}}),c.final().connectorCb(function(a){var d=a.height()/4,e=(b.el.height()/2+b.el.height()+c.el.height()/2)/2-a.height()/2,f=e-b.el.height()/2;return 0===c.winner().id?(j=f,i=3*d):1===c.winner().id?(j=f+2*d,i=d):(j=f+d,i=2*d),j+=a.height()/2,{height:-j,shift:-i}})}function r(b,c,d,e,f,g){var h=[],i=a('
');return{el:i,bracket:b,id:d,addMatch:function(a,c){var f,i=h.length;f=null!==a?a():[{source:b.round(d-1).match(2*i).winner},{source:b.round(d-1).match(2*i+1).winner}];var j=g(this,f,i,e?e[i]:null,c);return h.push(j),j},match:function(a){return h[a]},prev:function(){return c},size:function(){return h.length},render:function(){i.empty(),("function"!=typeof f||f())&&(i.appendTo(b.el),a.each(h,function(a,b){b.render()}))},results:function(){var b=[];return a.each(h,function(a,c){b.push(c.results())}),b}}}function s(b,c,d){var e=[];return{el:b,addRound:function(a){var b=e.length,f=null;b>0&&(f=e[b-1]);var g=r(this,f,b,c?c[b]:null,a,d);return e.push(g),g},dropRound:function(){e.pop()},round:function(a){return e[a]},size:function(){return e.length},"final":function(){return e[e.length-1].match(0)},winner:function(){return e[e.length-1].match(0).winner()},loser:function(){return e[e.length-1].match(0).loser()},render:function(){b.empty();for(var a=0;ab&&(g=!1,b=-b),2>b&&(b=0);var h=a('
').appendTo(d);h.css("height",b),h.css("width",f+"px"),h.css(e,-f-2+"px"),c>=0?h.css("top",c+"px"):h.css("bottom",-c+"px"),g?h.css("border-bottom","none"):h.css("border-top","none");var i=a('
').appendTo(h);return i.css("width",f+"px"),i.css(e,-f+"px"),g?i.css("bottom","0px"):i.css("top","0px"),h}function u(b,c,d){var e=a('
').appendTo(b),f=a('+').appendTo(e);if(f.click(function(){var a,b=c.teams.length;for(a=0;b>a;a+=1)c.teams.push(["",""]);return v(d)}),c.teams.length>1&&1===c.results.length||c.teams.length>2&&3===c.results.length){var g=a('-').appendTo(e);g.click(function(){return c.teams.length>1?(c.teams=c.teams.slice(0,c.teams.length/2),v(d)):void 0})}var h;1===c.results.length&&c.teams.length>1?(h=a('de').appendTo(e),h.click(function(){return c.teams.length>1&&c.results.length<3?(c.results.push([],[]),v(d)):void 0})):3===c.results.length&&c.teams.length>1&&(h=a('se').appendTo(e),h.click(function(){return 3===c.results.length?(c.results=c.results.slice(0,1),v(d)):void 0}))}var v=function(e){function f(a){m=0,v.render(),w&&x&&(w.render(),x.render()),j(y,v,x),a&&(r.results[0]=v.results(),w&&x&&(r.results[1]=w.results(),r.results[2]=x.results()),e.save&&e.save(r,e.userData))}function i(c,d,i,j,k){function l(c,d,i){var j,k=m,l=a('
');j=d.name&&i?b(d.score)?d.score:"--":"--",l.append(j),m+=1;var n=d.name?d.name:"--",p=a('
'),q=a('
').appendTo(p);return 0===c&&p.attr("data-resultid","team-"+k),e.decorator.render(q,n,j),b(d.idx)&&p.attr("data-teamid",d.idx),null===d.name?p.addClass("na"):g(o).name===d.name?p.addClass("win"):h(o).name===d.name&&p.addClass("lose"),p.append(l),null!==d.name&&i&&e.save&&e.save&&(q.addClass("editable"),q.click(function(){function b(){function h(h,i){h&&(e.init.teams[~~(d.idx/2)][d.idx%2]=h),f(!0),g.click(b);var j=e.el.find(".team[data-teamid="+(d.idx+1)+"] div.label:first");j.length&&i===!0&&0===c&&a(j).click()}g.unbind(),e.decorator.edit(g,d.name,h)}var g=a(this);b()}),d.name&&(l.addClass("editable"),l.click(function(){function c(){e.unbind();var g;g=b(d.score)?e.text():"0";var h=a('');h.val(g),e.html(h),h.focus().select(),h.keydown(function(c){b(a(this).val())?a(this).removeClass("error"):a(this).addClass("error");var d=c.keyCode||c.which;if(9===d||13===d||27===d){if(c.preventDefault(),a(this).blur(),27===d)return;var e=y.find("div.score[data-resultid=result-"+(k+1)+"]");e&&e.click()}}),h.blur(function(){var a=h.val();a&&b(a)||b(d.score)?a&&b(a)||!b(d.score)||(a=d.score):a="0",e.html(a),b(a)&&g!==parseInt(a,10)&&(d.score=parseInt(a,10),f(!0)),e.click(c)})}var e=a(this);c()}))),p}var o={a:d[0],b:d[1]},p=null,q=null,r=a('
'),s=a('
');if(!e.save){var u=j?j[2]:null;e.onMatchHover&&s.hover(function(){e.onMatchHover(u,!0)},function(){e.onMatchHover(u,!1)}),e.onMatchClick&&s.click(function(){e.onMatchClick(u)})}return o.a.id=0,o.b.id=1,o.a.name=o.a.source().name,o.b.name=o.b.source().name,o.a.score=j?j[0]:null,o.b.score=j?j[1]:null,o.a.name&&o.b.name||!b(o.a.score)&&!b(o.b.score)||(console.log("ERROR IN SCORE DATA: "+o.a.source().name+": "+o.a.score+", "+o.b.source().name+": "+o.b.score),o.a.score=o.b.score=null),{el:r,id:i,round:function(){return c},connectorCb:function(a){p=a},connect:function(a){var b,c,d=s.height()/4,e=r.height()/2;if(a&&null!==a){var f=a(s,this);if(null===f)return;b=f.shift,c=f.height}else 0===i%2?0===this.winner().id?(b=d,c=e):1===this.winner().id?(b=3*d,c=e-2*d):(b=2*d,c=e-d):0===this.winner().id?(b=3*-d,c=-e+2*d):1===this.winner().id?(b=-d,c=-e):(b=2*-d,c=-e+d);s.append(t(c,b,s,n))},winner:function(){return g(o)},loser:function(){return h(o)},first:function(){return o.a},second:function(){return o.b},setAlignCb:function(a){q=a},render:function(){r.empty(),s.empty(),o.a.name=o.a.source().name,o.b.name=o.b.source().name,o.a.idx=o.a.source().idx,o.b.idx=o.b.source().idx;var a=!1;!o.a.name&&""!==o.a.name||!o.b.name&&""!==o.b.name||(a=!0),g(o).name?s.removeClass("np"):s.addClass("np"),s.append(l(c.id,o.a,a)),s.append(l(c.id,o.b,a)),r.appendTo(c.el),r.append(s),this.el.css("height",c.bracket.el.height()/c.size()+"px"),s.css("top",this.el.height()/2-s.height()/2+"px"),q&&q(s);var b=!1;"function"==typeof k&&(b=k(this)),b||this.connect(p)},results:function(){return[o.a.score,o.b.score]}}}var m,n="lr"===e.dir?"right":"left";if(!e)throw Error("Options not set");if(!e.el)throw Error("Invalid jQuery object as container");if(!e.init&&!e.save)throw Error("No bracket data or save callback given");if(void 0===e.userData&&(e.userData=null),!(!e.decorator||e.decorator.edit&&e.decorator.render))throw Error("Invalid decorator input");e.decorator||(e.decorator={edit:k,render:l});var r;e.init||(e.init={teams:[["",""]],results:[]}),r=e.init;var v,w,x,y=a('
').appendTo(e.el.empty()),z=r.results;z=d(z,4-c(z)),r.results=z;var A=z.length<=1;e.skipSecondaryFinal&&A&&a.error("skipSecondaryFinal setting is viable only in double elimination mode"),e.save&&u(y,r,e);var B,C,D;A?C=a('
').appendTo(y):(B=a('
').appendTo(y),C=a('
').appendTo(y),D=a('
').appendTo(y));var E=64*r.teams.length;C.css("height",E),A&&r.teams.length<=2&&!e.skipConsolationRound&&(E+=40,y.css("height",E)),D&&D.css("height",C.height()/2);var F;return F=A?Math.log(2*r.teams.length)/Math.log(2):2*(Math.log(2*r.teams.length)/Math.log(2)-1)+1,e.save?y.css("width",140*F+40):y.css("width",140*F+10),v=s(C,z&&z[0]?z[0]:null,i),A||(w=s(D,z&&z[1]?z[1]:null,i),x=s(B,z&&z[2]?z[2]:null,i)),o(v,r.teams,A,e.skipConsolationRound),A||(p(v,w,r.teams.length),q(x,v,w,e.skipSecondaryFinal,e.skipConsolationRound,y)),f(!1),{data:function(){return e.init}}},w={init:function(b){var c=this;b.el=this,b.save&&(b.onMatchClick||b.onMatchHover)&&a.error("Match callbacks may not be passed in edit mode (in conjunction with save callback)"),b.dir=b.dir||"lr",b.skipConsolationRound=b.skipConsolationRound||!1,b.skipSecondaryFinal=b.skipSecondaryFinal||!1,"lr"!==b.dir&&"rl"!==b.dir&&a.error('Direction must be either: "lr" or "rl"');var d=v(b);return a(this).data("bracket",{target:c,obj:d}),d},data:function(){var b=a(this).data("bracket");return b.obj.data()}};a.fn.bracket=function(b){return w[b]?w[b].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof b&&b?(a.error("Method "+b+" does not exist on jQuery.bracket"),void 0):w.init.apply(this,arguments)}}(jQuery); 3 | -------------------------------------------------------------------------------- /client/js/vs.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var Game = require('./game'); 3 | var jsonpack = require('jsonpack'); 4 | var Map = require('./game/map'); 5 | 6 | $(function() { 7 | var url = location.href; 8 | var title = document.title; 9 | $('.js-weibo').attr('href', 'http://v.t.sina.com.cn/share/share.php?url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent(title)); 10 | $('.js-twitter').attr('href', 'http://twitter.com/home/?status=' + encodeURIComponent(title) + ':' + url); 11 | maps.forEach(function(map) { 12 | var mapModel = new Map(map, function() { 13 | $('.select-maps li').removeClass('is-selected'); 14 | $(this).parent().addClass('is-selected'); 15 | $('#playground').hide(); 16 | $('p.hint').show(); 17 | $('p.hint').html('载入地图“' + map.name + '”中...'); 18 | var url; 19 | if (map.result) { 20 | url = '/replay/' + map.result; 21 | } else { 22 | url = '/replay?user1=' + encodeURIComponent(user1.id) + '&user2=' + encodeURIComponent(user2.id) + '&map=' + map.id; 23 | } 24 | $.get(url, function(data) { 25 | $('p.hint').hide(); 26 | new Game(map.data.map, jsonpack.unpack(data), [user1.name, user2.name], 300, $('#playground')); 27 | $('#playground').show(); 28 | }).fail(function(res, _, err) { 29 | if (res.responseJSON && res.responseJSON.err) { 30 | alert(res.responseJSON.err); 31 | } else { 32 | alert(err); 33 | } 34 | $('p.hint').hide(); 35 | }); 36 | }); 37 | var $li = $('
  • '); 38 | mapModel.render($li, 200); 39 | $('.js-select-map').append($li); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /config/_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "mysql": { 3 | "database": "codegame", 4 | "username": "root", 5 | "password": "", 6 | "logging": false 7 | }, 8 | "redis": { 9 | "port": 6379 10 | }, 11 | "session": { 12 | "secret": "Session Secret" 13 | }, 14 | "github": { 15 | "id": "GitHub App Id", 16 | "secret": "GitHub App Secret" 17 | }, 18 | "googleAnalytics": null 19 | } 20 | -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | var mergeObject = function(obj1, obj2) { 2 | Object.keys(obj2).forEach(function(key) { 3 | obj1[key] = obj2[key]; 4 | }); 5 | }; 6 | 7 | mergeObject(global, require('./models')); 8 | 9 | var config = require('config'); 10 | global.$config = config; 11 | 12 | var async = require('async'); 13 | 14 | var game = require('./sandbox'); 15 | var gameQueue = async.queue(function(task, callback) { 16 | game(task.mapData, task.code1, task.code2, callback); 17 | }, 1); 18 | 19 | var jsonpack = require('jsonpack'); 20 | var crypto = require('crypto'); 21 | var md5 = function(value) { 22 | return crypto.createHash('md5').update(value).digest('hex'); 23 | }; 24 | 25 | global.Game = function(mapId, code1, code2, options, callback) { 26 | if (typeof options === 'function') { 27 | callback = options; 28 | options = null; 29 | } 30 | if (options && options.cache === false) { 31 | Map.find(mapId).done(function(err, map) { 32 | if (err) { 33 | return callback(err); 34 | } 35 | if (!map) { 36 | return callback(new Error('没有找到对应的地图')); 37 | } 38 | gameQueue.push({ mapData: map.parse(), code1: code1, code2: code2 }, callback); 39 | }); 40 | return; 41 | } 42 | var code1Md5 = md5(code1); 43 | var code2Md5 = md5(code2); 44 | Result.find({ where: { MapId: mapId, code1: code1Md5, code2: code2Md5 } }).done(function(err, result) { 45 | if (err) { 46 | return callback(err); 47 | } 48 | if (result) { 49 | return callback(null, jsonpack.unpack(result.data), result.data, result); 50 | } 51 | Map.find(mapId).done(function(err, map) { 52 | if (err) { 53 | return callback(err); 54 | } 55 | if (!map) { 56 | return callback(new Error('没有找到对应的地图')); 57 | } 58 | gameQueue.push({ mapData: map.parse(), code1: code1, code2: code2 }, function(err, replay) { 59 | if (err) { 60 | return callback(err); 61 | } 62 | var packedResult = jsonpack.pack(replay); 63 | Result.create({ 64 | code1: code1Md5, 65 | code2: code2Md5, 66 | data: packedResult, 67 | MapId: mapId 68 | }).done(function(err, result) { 69 | callback(err, replay, packedResult, result); 70 | }); 71 | }); 72 | }); 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | var NODE_ENV = process.env.NODE_ENV || 'development'; 4 | 5 | // CSS 6 | var myth = require('gulp-myth'); 7 | 8 | gulp.task('css', function() { 9 | return gulp.src('client/css/app.css') 10 | .pipe(myth({ 11 | source: 'client/css' 12 | })) 13 | .pipe(gulp.dest('public/css')); 14 | }); 15 | 16 | gulp.task('watch-css', function() { 17 | return gulp.watch('client/css/**/*.css', ['css']); 18 | }); 19 | 20 | // Image 21 | gulp.task('image', function() { 22 | return gulp.src('client/images/**/*') 23 | .pipe(gulp.dest('public/images')); 24 | }); 25 | 26 | gulp.task('watch-image', function() { 27 | return gulp.watch('client/images/**/*', ['image']); 28 | }); 29 | 30 | // JavaScript 31 | var browserify = require('gulp-browserify'); 32 | var uglify = require('gulp-uglify'); 33 | 34 | gulp.task('js', function() { 35 | var stream = gulp.src('client/js/*.js').pipe(browserify()); 36 | if (NODE_ENV === 'production') { 37 | stream.pipe(require('gulp-uglify')()); 38 | } 39 | stream.pipe(gulp.dest('public/js')); 40 | return stream; 41 | }); 42 | 43 | gulp.task('watch-js', function() { 44 | return gulp.watch('client/js/**/*.js', ['js']); 45 | }); 46 | 47 | gulp.task('build', ['css', 'js', 'image']); 48 | gulp.task('watch', ['watch-css', 'watch-js', 'watch-image']); 49 | -------------------------------------------------------------------------------- /models/code.js: -------------------------------------------------------------------------------- 1 | module.exports = function(DataTypes) { 2 | return [{ 3 | code: DataTypes.TEXT, 4 | rank: DataTypes.INTEGER, 5 | win: DataTypes.INTEGER, 6 | lost: DataTypes.INTEGER, 7 | winReason: DataTypes.STRING, 8 | loseReason: DataTypes.STRING 9 | }]; 10 | }; 11 | -------------------------------------------------------------------------------- /models/history.js: -------------------------------------------------------------------------------- 1 | module.exports = function(DataTypes) { 2 | return [{ 3 | host: DataTypes.INTEGER, 4 | challenger: DataTypes.INTEGER, 5 | result: { type: DataTypes.ENUM('win', 'lost') } 6 | }]; 7 | }; 8 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | var Sequelize = require('sequelize'); 2 | var inflection = require('inflection'); 3 | var config = require('config'); 4 | 5 | var sequelize = new Sequelize(config.mysql.database, 6 | config.mysql.username, 7 | config.mysql.password, 8 | config.mysql); 9 | 10 | var self = module.exports = {}; 11 | 12 | var models = require('node-require-directory')(__dirname); 13 | Object.keys(models).forEach(function(key) { 14 | if (key === 'index') { 15 | return; 16 | } 17 | var modelName = inflection.classify(key); 18 | var modelInstance = sequelize.import(modelName , function(sequelize, DataTypes) { 19 | var definition = [modelName].concat(models[key](DataTypes)); 20 | return sequelize.define.apply(sequelize, definition); 21 | }); 22 | self[modelName] = modelInstance; 23 | }); 24 | 25 | self.User.hasMany(self.Code); 26 | self.Code.belongsTo(self.User); 27 | self.History.belongsTo(self.Result); 28 | self.Result.belongsTo(self.Map); 29 | 30 | self.Tournament.hasMany(self.Map); 31 | self.Map.hasMany(self.Tournament); 32 | self.Tournament.hasMany(self.User); 33 | self.User.hasMany(self.Tournament); 34 | 35 | self.sequelize = self.DB = sequelize; 36 | sequelize.sync(); 37 | -------------------------------------------------------------------------------- /models/map.js: -------------------------------------------------------------------------------- 1 | module.exports = function(DataTypes) { 2 | return [{ 3 | name: { 4 | type: DataTypes.STRING, 5 | allowNull: false 6 | }, 7 | type: { type: DataTypes.ENUM('general', 'tournament', 'rank') }, 8 | theme: DataTypes.STRING, 9 | data: { 10 | type: DataTypes.TEXT, 11 | validate: { 12 | isValidMapData: parseMapData 13 | } 14 | } 15 | }, { 16 | instanceMethods: { 17 | parse: function() { 18 | return parseMapData(this.data); 19 | } 20 | } 21 | }]; 22 | }; 23 | 24 | function parseMapData(data) { 25 | var DIRECTION = ['up', 'right', 'down', 'left']; 26 | var result = { 27 | players: [] 28 | }; 29 | 30 | var cols; 31 | var mapData = data.split('|').map(function(line, lineIndex) { 32 | if (typeof cols === 'undefined') { 33 | cols = line.length; 34 | } 35 | 36 | if (line.length != cols) { 37 | throw Error('Not all rows have same length'); 38 | } 39 | return line.split('').map(function(c, charIndex) { 40 | var index; 41 | index = ['a', 'b', 'c', 'd'].indexOf(c); 42 | if (index !== -1) { 43 | result.players[0] = { 44 | direction: DIRECTION[index], 45 | position: [charIndex, lineIndex] 46 | }; 47 | return '.'; 48 | } 49 | index = ['A', 'B', 'C', 'D'].indexOf(c); 50 | if (index !== -1) { 51 | result.players[1] = { 52 | direction: DIRECTION[index], 53 | position: [charIndex, lineIndex] 54 | }; 55 | return '.'; 56 | } 57 | if (!(c == 'x' || c == 'o' || c == '.')) { 58 | throw Error('Invalid map tile type:' + c); 59 | } 60 | return c; 61 | }); 62 | }); 63 | 64 | if (!(result.players[0] && result.players[1])) { 65 | throw Error('Need starting location for two players'); 66 | } 67 | 68 | result.map = []; 69 | 70 | for (var j = 0; j < mapData.length; ++j) { 71 | for (var i = 0; i < mapData[j].length; ++i) { 72 | if (!result.map[i]) { 73 | result.map[i] = []; 74 | } 75 | result.map[i][j] = mapData[j][i]; 76 | } 77 | } 78 | 79 | return result; 80 | } 81 | -------------------------------------------------------------------------------- /models/result.js: -------------------------------------------------------------------------------- 1 | module.exports = function(DataTypes) { 2 | return [{ 3 | code1: DataTypes.STRING, 4 | code2: DataTypes.STRING, 5 | data: DataTypes.TEXT 6 | }]; 7 | }; 8 | -------------------------------------------------------------------------------- /models/tournament.js: -------------------------------------------------------------------------------- 1 | module.exports = function(DataTypes) { 2 | return [{ 3 | name: DataTypes.STRING, 4 | description: DataTypes.TEXT, 5 | start: DataTypes.DATE, 6 | end: DataTypes.DATE, 7 | result: DataTypes.TEXT 8 | }]; 9 | }; 10 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | var appAdmins = require('config').admins || []; 2 | module.exports = function(DataTypes) { 3 | return [{ 4 | id: { type: DataTypes.INTEGER, primaryKey: true }, 5 | login: DataTypes.STRING, 6 | name: DataTypes.STRING, 7 | avatar: DataTypes.STRING, 8 | github: DataTypes.STRING, 9 | blog: DataTypes.STRING, 10 | location: DataTypes.STRING, 11 | company: DataTypes.STRING, 12 | email: DataTypes.STRING, 13 | bio: DataTypes.STRING 14 | }, { 15 | instanceMethods: { 16 | isInTournament: function() { 17 | return this.getTournaments().then(function(tournaments) { 18 | return tournaments.some(function(tournament) { 19 | var now = new Date(); 20 | return tournament.start < now && tournament.end > now; 21 | }); 22 | }); 23 | }, 24 | 25 | isAdmin: function() { 26 | return appAdmins.indexOf(this.login) != -1; 27 | } 28 | } 29 | }]; 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CodeGame", 3 | "version": "1.1.0", 4 | "author": { 5 | "name": "Zihua Li", 6 | "email": "i@zihua.li", 7 | "url": "http://zihua.li" 8 | }, 9 | "private": true, 10 | "scripts": { 11 | "watch": "gulp watch", 12 | "build": "gulp build", 13 | "rank": "NODE_ENV=production node services/calc_rank.js", 14 | "tournament": "NODE_ENV=production node services/calc_tournament.js" 15 | }, 16 | "dependencies": { 17 | "aqsort": "^1.1.0", 18 | "async": "^0.9.0", 19 | "body-parser": "~1.0.0", 20 | "codemirror": "^4.6.0", 21 | "config": "^1.2.1", 22 | "connect-redis": "^2.1.0", 23 | "cookie-parser": "~1.0.1", 24 | "debug": "~0.7.4", 25 | "express": "^4.0.0", 26 | "express-session": "^1.8.2", 27 | "inflection": "^1.5.0", 28 | "jade": "~1.3.0", 29 | "jquery": "^2.1.1", 30 | "jquery.transit": "^0.9.12", 31 | "jshint": "^2.5.6", 32 | "jsonpack": "^1.1.2", 33 | "moment": "^2.8.4", 34 | "morgan": "~1.0.0", 35 | "mysql": "^2.5.2", 36 | "node-require-directory": "^1.0.2", 37 | "octonode": "^0.6.6", 38 | "seedrandom": "~2.3.10", 39 | "sequelize": "^2.0.0-rc1", 40 | "static-favicon": "~1.0.0" 41 | }, 42 | "devDependencies": { 43 | "gulp": "^3.8.8", 44 | "gulp-browserify": "^0.5.0", 45 | "gulp-myth": "^1.0.1", 46 | "gulp-sketch": "0.0.6", 47 | "gulp-uglify": "^1.0.1" 48 | }, 49 | "jshintConfig": { 50 | "undef": true, 51 | "unused": true, 52 | "node": true, 53 | "predef": [ 54 | "Map", 55 | "Result", 56 | "Tournament", 57 | "Code", 58 | "History", 59 | "User", 60 | "Game", 61 | "Promise" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /routes/account.js: -------------------------------------------------------------------------------- 1 | var app = module.exports = require('express')(); 2 | var config = require('config'); 3 | 4 | var github = require('octonode'); 5 | var authURL = github.auth.config({ 6 | id: config.github.id, 7 | secret: config.github.secret 8 | }).login([]); 9 | 10 | app.get('/github', function(req, res) { 11 | res.redirect(authURL); 12 | }); 13 | 14 | app.get('/github/callback', function(req, res) { 15 | github.auth.login(req.query.code, function(err, token) { 16 | var client = github.client(token); 17 | client.me().info(function(err, info) { 18 | if (err || !info) { 19 | return res.end(err.message); 20 | } 21 | User.findOrCreate({ 22 | where: { 23 | id: info.id 24 | }, 25 | defaults: { 26 | login: info.login, 27 | name: info.name || info.login, 28 | avatar: info.avatar_url, 29 | github: info.html_url, 30 | blog: info.blog, 31 | location: info.location, 32 | company: info.company, 33 | email: info.email, 34 | bio: info.bio 35 | } 36 | }).then(function() { 37 | req.session.user = info.id; 38 | res.redirect('/' + info.login); 39 | }); 40 | }); 41 | }); 42 | }); 43 | 44 | app.get('/logout', function(req, res) { 45 | req.session.destroy(); 46 | res.redirect('/'); 47 | }); 48 | -------------------------------------------------------------------------------- /routes/code.js: -------------------------------------------------------------------------------- 1 | var app = module.exports = require('express')(); 2 | var jsonpack = require('jsonpack'); 3 | 4 | var Promise = require('sequelize').Promise; 5 | 6 | app.post('/preview', function(req, res) { 7 | if (!req.me) { 8 | return res.status(403).json({ err: '请先登录' }); 9 | } 10 | var promise; 11 | if (req.body.enemy) { 12 | promise = User.find({ where: { login: req.body.enemy } }).then(function(user) { 13 | if (!user) { 14 | throw new Error('用户名不存在(用户名是对方主页网址中的标识符)'); 15 | } 16 | return user.isInTournament().then(function(result) { 17 | if (result) { 18 | throw new Error('对方正在参与杯赛期间,无法进行对战'); 19 | } 20 | return Code.find({ where: { UserId: user.id }}).then(function(code) { 21 | if (!code) { 22 | throw new Error('用户没有公开的代码'); 23 | } 24 | code = code.dataValues; 25 | code.name = user.name; 26 | res.locals.enemy = code; 27 | }); 28 | }); 29 | }); 30 | } else { 31 | promise = Promise.resolve(); 32 | } 33 | promise.then(function() { 34 | return Map.find(req.body.map).then(function(map) { 35 | if (!map) { 36 | throw new Error('未找到地图'); 37 | } 38 | res.locals.map = map.parse().map; 39 | }); 40 | }); 41 | promise.then(function() { 42 | var name = req.me.name + '(预览)'; 43 | res.locals.enemy = res.locals.enemy || { name: name, code: req.body.code }; 44 | Game(req.body.map, req.body.code, res.locals.enemy.code, { cache: false }, function(err, result) { 45 | res.json({ 46 | result: jsonpack.pack(result), 47 | names: [name, res.locals.enemy.name], 48 | map: res.locals.map 49 | }); 50 | }); 51 | }).catch(function(e) { 52 | res.status(400).json({ err: e.message }); 53 | }); 54 | 55 | }); 56 | 57 | app.post('/', function(req, res) { 58 | if (!req.me) { 59 | return res.status(403).json({ err: '请先登录' }); 60 | } 61 | Code.find({ where: { UserId: req.me.id } }).then(function(code) { 62 | if (!code) { 63 | code = Code.build({ UserId: req.me.id }); 64 | } 65 | code.code = req.body.code; 66 | code.save(); 67 | res.json({ msg: 'Success' }); 68 | }); 69 | }); 70 | 71 | app.get('/editor', function(req, res) { 72 | if (!req.me) { 73 | return res.redirect('/account/github'); 74 | } 75 | req.me.getCodes().then(function(codes) { 76 | res.locals.code = codes.length ? codes[0].code : null; 77 | return Map.findAll({ where: { type: 'general' }, attributes: ['id', 'name'] }); 78 | }).then(function(maps) { 79 | res.locals.maps = maps; 80 | res.render('editor'); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /routes/defaults.js: -------------------------------------------------------------------------------- 1 | var app = module.exports = require('express')(); 2 | 3 | app.get('/:user', function(req, res, next) { 4 | User.find({ where: { login: req.params.user } }).done(function(err, user) { 5 | if (!user) { 6 | return next(); 7 | } 8 | user.getCodes().then(function(codes) { 9 | var code; 10 | if (codes.length) { 11 | code = codes[0]; 12 | } 13 | res.render('user', { user: user, code: code }); 14 | }); 15 | }); 16 | }); 17 | 18 | app.get('/:user1/vs/:user2', function(req, res) { 19 | User.findAll({ where: { login: [req.params.user1, req.params.user2] } }).then(function(users) { 20 | var user1, user2; 21 | users.forEach(function(user) { 22 | if (user.login === req.params.user1) { 23 | user1 = user; 24 | } 25 | if (user.login === req.params.user2) { 26 | user2 = user; 27 | } 28 | }); 29 | if (!user1 || !user2) { 30 | res.json({ err: '玩家不存在' }); 31 | } 32 | res.locals.user1 = { id: user1.id, name: user1.name, login: user1.login }; 33 | res.locals.user2 = { id: user2.id, name: user2.name, login: user2.login }; 34 | res.locals.title = '坦克 AI 对战(' + user1.name + ' VS ' + user2.name + ')'; 35 | Map.findAll({ where: { type: 'general' } }).then(function(maps) { 36 | res.locals.maps = maps.map(function(map) { 37 | return { 38 | id: map.id, 39 | name: map.name, 40 | data: map.parse() 41 | }; 42 | }); 43 | res.render('vs'); 44 | }); 45 | }); 46 | }); 47 | 48 | app.get('/', function(req, res) { 49 | if (req.me) { 50 | return res.redirect('/' + req.me.login); 51 | } 52 | res.render('index'); 53 | }); 54 | -------------------------------------------------------------------------------- /routes/doc.js: -------------------------------------------------------------------------------- 1 | var app = module.exports = require('express')(); 2 | 3 | app.get('/', function(req, res) { 4 | res.render('doc/rule'); 5 | }); 6 | 7 | app.get('/api', function(req, res) { 8 | res.render('doc/api'); 9 | }); 10 | 11 | app.get('/tournament', function(req, res) { 12 | res.render('doc/tournament'); 13 | }); 14 | -------------------------------------------------------------------------------- /routes/history.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | var app = module.exports = require('express')(); 4 | 5 | app.get('/:id', function(req, res) { 6 | History.find({ 7 | where: { id: req.params.id }, 8 | include: [{ model: Result }] 9 | }).then(function(history) { 10 | if (!history) { 11 | return res.status(404).json({ err: 'Not found' }); 12 | } 13 | history.Result.getMap().then(function(map) { 14 | res.locals.map = map; 15 | async.map([history.host, history.challenger], function(userId, next) { 16 | User.find(userId).done(next); 17 | }, function(err, users) { 18 | res.locals.title = 'AI 对战录像(' + users[0].name + ' VS ' + users[1].name + ')- ' + map.name; 19 | res.render('history', { history: history, users: users.map(function(user) { return user.name; }) }); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var routes = require('node-require-directory')(__dirname); 2 | 3 | module.exports = function(app) { 4 | app.use(function(req, res, next) { 5 | res.locals.me = null; 6 | if (req.session.user) { 7 | User.find({ where: { id: req.session.user } }).then(function(user) { 8 | res.locals.me = req.me = user; 9 | next(); 10 | }); 11 | } else { 12 | next(); 13 | } 14 | }); 15 | 16 | Object.keys(routes).forEach(function(key) { 17 | if (key === 'index') { 18 | return; 19 | } 20 | if (key === 'defaults') { 21 | return app.use(routes.defaults); 22 | } 23 | app.use('/' + key, routes[key]); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /routes/map.js: -------------------------------------------------------------------------------- 1 | var app = module.exports = require('express')(); 2 | 3 | testIsAdmin = function(req,res,next) { 4 | if(!(req.me && req.me.isAdmin())) { 5 | res.end("you are not an admin"); 6 | } else { 7 | next(); 8 | } 9 | } 10 | 11 | 12 | app.get('/', testIsAdmin, function(req, res) { 13 | Map.findAll().then(function(maps) { 14 | res.locals.maps = maps; 15 | res.render("map/index"); 16 | }); 17 | }); 18 | 19 | app.get('/:id/edit', testIsAdmin, function(req, res) { 20 | Map.find(req.params.id).then(function(map) { 21 | res.locals.map = map.data && map.parse(); 22 | res.locals.mapId = req.params.id; 23 | res.render("map/edit"); 24 | }).catch(function(err) { 25 | console.error(err); 26 | res.send(404); 27 | }); 28 | }); 29 | 30 | app.post('/create', testIsAdmin, function(req, res) { 31 | Map.create(req.body).then(function(map) { 32 | res.redirect("/map/"+map.id+"/edit"); 33 | }).catch(function(errs) { 34 | res.send(400,"Invalid map"); 35 | }); 36 | 37 | }); 38 | 39 | app.post('/update', testIsAdmin, function(req, res) { 40 | Map.find(req.body.id).then(function(map) { 41 | map.data = req.body.data; 42 | return map.save(); 43 | }).then(function() { 44 | res.redirect("/map/"+req.body.id+"/edit"); 45 | }).catch(function(err) { 46 | console.log(err); 47 | throw err; 48 | }); 49 | }); -------------------------------------------------------------------------------- /routes/rank.js: -------------------------------------------------------------------------------- 1 | var app = module.exports = require('express')(); 2 | 3 | app.get('/', function(req, res) { 4 | Code.findAll({ 5 | where: 'rank IS NOT NULL', 6 | limit: 100, 7 | order: 'rank ASC', 8 | include: [{ model: User, include: [{ model: Tournament }]}] 9 | }).done(function(err, rank) { 10 | res.render('rank', { rank: rank }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /routes/replay.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | var app = module.exports = require('express')(); 4 | app.get('/', function(req, res) { 5 | var user1Id = parseInt(req.query.user1, 10); 6 | var user2Id = parseInt(req.query.user2, 10); 7 | async.map([user1Id, user2Id], function(id, callback) { 8 | function getCode() { 9 | Code.find({ where: { UserId: id } }).done(function(err, code) { 10 | callback(null, code ? code.code : null); 11 | }); 12 | } 13 | if (req.me && req.me.id === id) { 14 | return getCode(); 15 | } 16 | User.find(id).then(function(user) { 17 | user.isInTournament().then(function(result) { 18 | if (result) { 19 | return callback(user); 20 | } 21 | getCode(); 22 | }); 23 | }); 24 | }, function(err, codes) { 25 | if (err) { 26 | return res.status(400).json({ err: '用户在参与杯赛期间不能参与 PvP 对战' }); 27 | } 28 | if (codes[0] && codes[1]) { 29 | Game(req.query.map, codes[0], codes[1], function(err, replay, packedReplay, result) { 30 | res.json(packedReplay); 31 | // Add history 32 | if (user1Id !== user2Id && req.me && user2Id === req.me.id) { 33 | History.create({ 34 | host: user1Id, 35 | challenger: req.me.id, 36 | result: replay.meta.result.winner === 1 ? 'win' : 'lost', 37 | ResultId: result.id 38 | }).done(); 39 | } 40 | }); 41 | } else { 42 | res.status(400).json({ err: '用户不存在或者没有发布过代码' }); 43 | } 44 | }); 45 | }); 46 | 47 | app.get('/:replayId', function(req, res) { 48 | Result.find(req.params.replayId).then(function(result) { 49 | if (!result) { 50 | return res.status(404).json({ err: '录像不存在' }); 51 | } 52 | res.json(result.data); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /routes/tournaments.js: -------------------------------------------------------------------------------- 1 | var app = module.exports = require('express')(); 2 | var moment = require('moment'); 3 | 4 | app.get('/', function(req, res) { 5 | Tournament.findAll().then(function(tournaments) { 6 | res.locals.tournaments = tournaments; 7 | res.render('tournaments/index'); 8 | }); 9 | }); 10 | 11 | app.get('/:tournamentId', function(req, res) { 12 | Tournament.find({ 13 | where: { id: req.params.tournamentId }, 14 | include: [{ model: User }] 15 | }).then(function(tournament) { 16 | res.locals.moment = moment; 17 | res.locals.tournament = tournament; 18 | res.render('tournaments/show'); 19 | }); 20 | }); 21 | 22 | app.post('/:tournamentId/action/join', function(req, res) { 23 | if (!req.me) { 24 | return res.status(403).json({ err: '请先登录' }); 25 | } 26 | function next() { 27 | res.redirect('/tournaments/' + req.params.tournamentId); 28 | } 29 | Tournament.find(req.params.tournamentId).then(function(tournament) { 30 | tournament.hasUser(req.me).then(function(result) { 31 | if (result) { 32 | return next(); 33 | } 34 | tournament.addUser(req.me).then(function() { 35 | next(); 36 | }); 37 | }); 38 | }); 39 | }); 40 | 41 | app.post('/:tournamentId/action/leave', function(req, res) { 42 | if (!req.me) { 43 | return res.status(403).json({ err: '请先登录' }); 44 | } 45 | function next() { 46 | res.redirect('/tournaments/' + req.params.tournamentId); 47 | } 48 | Tournament.find(req.params.tournamentId).then(function(tournament) { 49 | tournament.removeUser(req.me).then(function() { 50 | next(); 51 | }); 52 | }); 53 | }); 54 | 55 | app.get('/:tournamentId/replays/:id', function(req, res) { 56 | Tournament.find(req.params.tournamentId).then(function(tournament) { 57 | if (!tournament.result) { 58 | return res.status(400).json({ err: '比赛尚未有结果' }); 59 | } 60 | if ((new Date()) < tournament.end && !(req.me && req.me.isAdmin())) { 61 | return res.status(400).json({ err: '比赛尚未有结果' }); 62 | } 63 | var result = JSON.parse(tournament.result).results; 64 | var fight; 65 | result.every(function(round) { 66 | return round.every(function(subRound) { 67 | if (subRound[2] && subRound[2].id === parseInt(req.params.id, 10)) { 68 | fight = subRound[2]; 69 | return false; 70 | } 71 | return true; 72 | }); 73 | }); 74 | if (!fight) { 75 | return res.status(400).json({ err: '未找到相关录像' }); 76 | } 77 | res.locals.title = tournament.name + ' AI 对战录像(' + fight.users[0].name + ' VS ' + fight.users[1].name + ')'; 78 | Map.findAll({ where: { id: fight.maps } }).then(function(maps) { 79 | res.locals.maps = maps.map(function(map, index) { 80 | return { 81 | id: map.id, 82 | name: map.name, 83 | data: map.parse(), 84 | result: fight.result[index] 85 | }; 86 | }); 87 | res.locals.user1 = fight.users[0]; 88 | res.locals.user2 = fight.users[1]; 89 | res.render('vs'); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /sandbox/game/commander.js: -------------------------------------------------------------------------------- 1 | var Commander = module.exports = function(player) { 2 | for (var key in player) { 3 | if (player.hasOwnProperty(key)) { 4 | this[key] = player[key]; 5 | } 6 | } 7 | this.__queue = []; 8 | }; 9 | 10 | Commander.prototype.__push = function(command) { 11 | this.__queue.push(command); 12 | }; 13 | 14 | Commander.prototype.turn = function(direction) { 15 | this.__push(direction); 16 | }; 17 | 18 | Commander.prototype.go = function(steps) { 19 | if (typeof steps !== 'number') { 20 | steps = 1; 21 | } 22 | while (steps--) { 23 | this.__push('go'); 24 | } 25 | }; 26 | 27 | Commander.prototype.fire = function() { 28 | this.__push('fire'); 29 | }; 30 | -------------------------------------------------------------------------------- /sandbox/game/game.js: -------------------------------------------------------------------------------- 1 | var Player = require('./player'); 2 | 3 | var Game = module.exports = function(parsedMap, options) { 4 | this.players = parsedMap.players.map(function(player, index) { 5 | return new Player(player.direction, player.position, options.AI[index]); 6 | }); 7 | 8 | this.map = parsedMap.map; 9 | 10 | this.frames = 0; 11 | 12 | this.star = null; 13 | this.lastCollectedStar = Number.NEGATIVE_INFINITY; 14 | }; 15 | 16 | Game.prototype.clone = function() { 17 | return { 18 | players: this.players.map(function(player) { 19 | return player.clone(); 20 | }), 21 | map: this.map.slice().map(function(line) { 22 | return line.slice(); 23 | }), 24 | frames: this.frames, 25 | star: this.star ? this.star.slice() : null 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /sandbox/game/movable.js: -------------------------------------------------------------------------------- 1 | var Movable = module.exports = function(direction, position) { 2 | this.direction = direction; 3 | this.position = position; 4 | this.id = (Math.random() * 100000000 | 0).toString(16); 5 | }; 6 | 7 | Movable.create = function(movable) { 8 | var clone = movable.clone(); 9 | return new Movable(clone.direction, clone.position); 10 | }; 11 | 12 | Movable.prototype.collided = function(movable) { 13 | return this.position[0] === movable.position[0] && 14 | this.position[1] === movable.position[1]; 15 | }; 16 | 17 | Movable.prototype.go = function(steps) { 18 | if (typeof steps === 'number' && steps > 1) { 19 | while (steps--) { 20 | this.go(); 21 | } 22 | return; 23 | } 24 | 25 | switch (this.direction) { 26 | case 'up': 27 | this.position[1] -= 1; 28 | break; 29 | case 'down': 30 | this.position[1] += 1; 31 | break; 32 | case 'left': 33 | this.position[0] -= 1; 34 | break; 35 | case 'right': 36 | this.position[0] += 1; 37 | break; 38 | } 39 | }; 40 | 41 | Movable.prototype.turn = function(direction) { 42 | if (this.direction === 'left' || this.direction === 'right') { 43 | if (this.direction === direction) { 44 | this.direction = 'down'; 45 | } else { 46 | this.direction = 'up'; 47 | } 48 | } else if (this.direction === 'up') { 49 | this.direction = direction; 50 | } else if (direction === 'left') { 51 | this.direction = 'right'; 52 | } else { 53 | this.direction = 'left'; 54 | } 55 | }; 56 | 57 | Movable.prototype.clone = function() { 58 | return { 59 | id: this.id, 60 | position: this.position.slice(), 61 | direction: this.direction 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /sandbox/game/player.js: -------------------------------------------------------------------------------- 1 | var vm = require('vm'); 2 | var Sandbox = require('./sandbox'); 3 | var Movable = require('./movable'); 4 | var seedrandom = require('seedrandom'); 5 | 6 | var crypto = require('crypto'); 7 | var md5 = function(value) { 8 | return crypto.createHash('md5').update(value).digest('hex'); 9 | }; 10 | 11 | var Player = module.exports = function(direction, position, code) { 12 | this.tank = new Movable(direction, position); 13 | 14 | this.bullet = null; 15 | this.stars = 0; 16 | this.pendingCommands = []; 17 | 18 | this.code = code; 19 | this.runTime = 0; 20 | 21 | this.error = null; 22 | this.logs = []; 23 | this.logLength = 0; 24 | this.logCount = 0; 25 | 26 | this.sandbox = new Sandbox(); 27 | var _this = this; 28 | this.sandbox.Math.random = function() { 29 | if (typeof _this.random === 'undefined') { 30 | _this.random = seedrandom(md5(code)); 31 | } 32 | return _this.random.apply(this, arguments); 33 | }; 34 | var start = Date.now(); 35 | try { 36 | vm.createScript(code).runInNewContext(this.sandbox, { 37 | timeout: 1500 38 | }); 39 | } catch (e) { 40 | this.error = e; 41 | this._log('error', JSON.stringify(e.message)); 42 | } 43 | this.runTime += Date.now() - start; 44 | 45 | if (!this.error && (!this.sandbox.onIdle && typeof onIdle !== 'function')) { 46 | this.error = new Error('Cannot find function "onIdle".'); 47 | } 48 | }; 49 | 50 | Player.prototype._log = function(type, data, frame) { 51 | this.logs.push({ 52 | type: type, 53 | data: data, 54 | frame: frame || 0, 55 | runTime: this.runTime 56 | }); 57 | }; 58 | 59 | Player.prototype.onIdle = function(self, enemy, game) { 60 | var code = 'onIdle(__self, __enemy, __game);'; 61 | if (!this.script) { 62 | this.script = vm.createScript(code); 63 | } 64 | var start = Date.now(); 65 | try { 66 | var _this = this; 67 | this.sandbox.__self = self; 68 | this.sandbox.__enemy = enemy; 69 | this.sandbox.__game = game; 70 | 71 | if (this.stopLog) { 72 | this.sandbox.print = function() {}; 73 | } else { 74 | this.sandbox.print = function(data) { 75 | try { 76 | var json = JSON.stringify(data); 77 | if (typeof json === 'undefined') { 78 | return; 79 | } 80 | _this.logLength += json.length; 81 | _this.logCount += 1; 82 | if (_this.logLength > 100000 || _this.logCount > 256) { 83 | _this._log('warn', JSON.stringify('日志长度超限,之后的日志将被忽略'), game.frames); 84 | _this.stopLog = true; 85 | _this.sandbox.print = function() {}; 86 | } else { 87 | _this._log('debug', json, game.frames); 88 | } 89 | } catch (err) { 90 | _this.error = err; 91 | _this._log('error', JSON.stringify(err.message), game.frames); 92 | } 93 | return; 94 | }; 95 | } 96 | this.script.runInNewContext(this.sandbox, { 97 | timeout: 1500 98 | }); 99 | } catch (err) { 100 | this.error = err; 101 | this._log('error', JSON.stringify(err.message), game.frames); 102 | } 103 | this.runTime += Date.now() - start; 104 | }; 105 | 106 | Player.prototype.clone = function() { 107 | return { 108 | tank: this.tank.clone(), 109 | bullet: this.bullet ? this.bullet.clone() : null, 110 | stars: this.stars 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /sandbox/game/replay.js: -------------------------------------------------------------------------------- 1 | var Replay = module.exports = function(game) { 2 | this.game = game; 3 | this.data = { 4 | meta: { 5 | players: game.players.map(function(player) { 6 | return { 7 | tank: player.tank.clone() 8 | }; 9 | }) 10 | }, 11 | records: [] 12 | }; 13 | }; 14 | 15 | Replay.prototype.record = function(data) { 16 | this.getRecord().push(data); 17 | }; 18 | 19 | Replay.prototype.getRecord = function() { 20 | var key = this.game.frames - 1; 21 | if (typeof this.data.records[key] === 'undefined') { 22 | this.data.records[key] = []; 23 | } 24 | return this.data.records[key]; 25 | }; 26 | 27 | Replay.prototype.end = function(result) { 28 | this.data.meta.result = result; 29 | for (var i = 0; i < this.game.frames - 1; ++i) { 30 | if (typeof this.data.records[i] === 'undefined') { 31 | this.data.records[i] = []; 32 | } 33 | } 34 | }; 35 | 36 | Replay.prototype.setRecord = function(record) { 37 | this.data.records[this.game.frames - 1] = record; 38 | }; 39 | 40 | Replay.prototype.clone = function() { 41 | var _this = this; 42 | this.game.players.forEach(function(player, index) { 43 | var metaPlayer = _this.data.meta.players[index]; 44 | metaPlayer.runTime = player.runTime; 45 | metaPlayer.logs = player.logs; 46 | }); 47 | this.data.records = this.data.records.map(function(record) { 48 | return Array.isArray(record) ? record : []; 49 | }); 50 | return this.data; 51 | }; 52 | -------------------------------------------------------------------------------- /sandbox/game/sandbox.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sandbox) { 2 | var sandMethod = function(method, thisObj) { 3 | if (typeof thisObj === 'undefined') { 4 | thisObj = null; 5 | } 6 | return function() { 7 | return method.apply(thisObj, arguments); 8 | }; 9 | }; 10 | var math = this.Math = {}; 11 | ['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 12 | 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'round', 'sin', 'sqrt', 'tan'].forEach(function(method) { 13 | math[method] = sandMethod(Math[method]); 14 | }); 15 | 16 | this.parseInt = sandMethod(parseInt); 17 | this.parseFloat = sandMethod(parseFloat); 18 | this.Date = null; 19 | 20 | for (var key in sandbox) { 21 | if (sandbox.hasOwnProperty(key)) { 22 | this[key] = sandbox[key]; 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /sandbox/index.js: -------------------------------------------------------------------------------- 1 | var Game = require('./game/game'); 2 | var Commander = require('./game/commander'); 3 | var Movable = require('./game/movable'); 4 | var Replay = require('./game/replay'); 5 | 6 | var STAR_INTERVAL = 10; 7 | var TOTAL_FRAMES = 128; 8 | var TOTAL_TIME = 3000; 9 | 10 | module.exports = function(parsedMap, code1, code2, callback) { 11 | var game = new Game(parsedMap, { 12 | AI: [code1, code2] 13 | }); 14 | 15 | var replay = new Replay(game); 16 | 17 | function checkError() { 18 | var errorIndex = []; 19 | game.players.forEach(function(player, index) { 20 | if (player.error) { 21 | errorIndex.push(index); 22 | } 23 | }); 24 | return errorIndex; 25 | } 26 | 27 | function handleDraw() { 28 | var winner, reason; 29 | if (game.players[0].stars !== game.players[1].stars) { 30 | reason = 'star'; 31 | if (game.players[0].stars > game.players[1].stars) { 32 | winner = 0; 33 | } else { 34 | winner = 1; 35 | } 36 | } else { 37 | reason = 'runTime'; 38 | if (game.players[0].runTime < game.players[1].runTime) { 39 | winner = 0; 40 | } else { 41 | winner = 1; 42 | } 43 | } 44 | return { 45 | type: 'game', 46 | action: 'end', 47 | reason: reason, 48 | winner: winner 49 | }; 50 | } 51 | 52 | function update(callback) { 53 | game.frames += 1; 54 | 55 | var errorIndex = checkError(); 56 | if (errorIndex.length === 2) { 57 | replay.end(handleDraw()); 58 | callback(null, replay.clone()); 59 | return; 60 | } else if (errorIndex.length === 1) { 61 | replay.end({ 62 | type: 'game', 63 | action: 'end', 64 | reason: 'error', 65 | winner: 1 - errorIndex[0] 66 | }); 67 | callback(null, replay.clone()); 68 | return; 69 | } 70 | 71 | // Check if any tank has crashed 72 | var crashedIndex = []; 73 | game.players.forEach(function(player, index) { 74 | if (player.tank.crashed) { 75 | crashedIndex.push(index); 76 | } 77 | }); 78 | 79 | if (crashedIndex.length === 2) { 80 | replay.end(handleDraw()); 81 | callback(null, replay.clone()); 82 | return; 83 | } 84 | 85 | if (crashedIndex.length === 1) { 86 | replay.end({ 87 | type: 'game', 88 | action: 'end', 89 | reason: 'crashed', 90 | winner: 1 - crashedIndex[0] 91 | }); 92 | callback(null, replay.clone()); 93 | return; 94 | } 95 | 96 | // Check if time's up 97 | if (game.frames > TOTAL_FRAMES) { 98 | replay.end(handleDraw()); 99 | callback(null, replay.clone()); 100 | return; 101 | } 102 | 103 | // Check runtime 104 | if (game.players.some(function(player) { return player.runTime > TOTAL_TIME; })) { 105 | var winner; 106 | if (game.players[0].runTime > game.players[1].runTime) { 107 | winner = 1; 108 | } else { 109 | winner = 0; 110 | } 111 | replay.end({ 112 | type: 'game', 113 | action: 'end', 114 | reason: 'timeout', 115 | value: game.players[1 - winner].runTime, 116 | winner: winner 117 | }); 118 | callback(null, replay.clone()); 119 | return; 120 | } 121 | 122 | // Place the star 123 | if (!game.star && (game.frames - game.lastCollectedStar >= STAR_INTERVAL)) { 124 | var middlePoint = [(game.players[0].tank.position[0] + game.players[1].tank.position[0]) / 2, 125 | (game.players[0].tank.position[1] + game.players[1].tank.position[1]) / 2]; 126 | 127 | if (middlePoint[0] % 1 === 0 && middlePoint[1] % 1 === 0 && 128 | game.map[middlePoint[0]][middlePoint[1]] !== 'x') { 129 | game.star = middlePoint; 130 | replay.record({ 131 | type: 'star', 132 | action: 'created', 133 | position: game.star 134 | }); 135 | } 136 | } 137 | 138 | // Execute a command 139 | game.players.forEach(function(player) { 140 | player.tank.lastPosition = null; 141 | var command = player.pendingCommands.shift(); 142 | switch (command) { 143 | case 'left': 144 | case 'right': 145 | player.tank.turn(command); 146 | replay.record({ 147 | type: 'tank', 148 | action: 'turn', 149 | direction: command, 150 | objectId: player.tank.id 151 | }); 152 | break; 153 | case 'go': 154 | player.tank.lastPosition = player.tank.position.slice(); 155 | player.tank.go(); 156 | replay.record({ 157 | type: 'tank', 158 | action: 'go', 159 | position: player.tank.position.slice(), 160 | objectId: player.tank.id 161 | }); 162 | break; 163 | case 'fire': 164 | if (!player.bullet) { 165 | player.bullet = Movable.create(player.tank); 166 | replay.record({ 167 | type: 'bullet', 168 | action: 'created', 169 | tank: player.tank.clone(), 170 | objectId: player.bullet.id 171 | }); 172 | } 173 | break; 174 | } 175 | }); 176 | 177 | // Handle collision 178 | var collidedPlayers; 179 | var testCollision = function(player, index) { 180 | if (!player.tank.lastPosition) { 181 | return; 182 | } 183 | var enemyTank = game.players[1 - index].tank; 184 | if (game.map[player.tank.position[0]][player.tank.position[1]] === 'x') { 185 | collidedPlayers.push(player); 186 | } else if (player.tank.collided(enemyTank)) { 187 | collidedPlayers.push(player); 188 | } else if (player.tank.lastPosition && enemyTank.lastPosition) { 189 | // 双方坦克互相穿过的情形 190 | if (player.tank.collided({ position: enemyTank.lastPosition }) && 191 | enemyTank.collided({ position: player.tank.lastPosition})) { 192 | collidedPlayers.push(player); 193 | } 194 | } 195 | }; 196 | var handleCollision = function(player) { 197 | replay.setRecord(replay.getRecord().filter(function(r) { 198 | return !(r.type === 'tank' && r.action === 'go' && r.objectId === player.tank.id); 199 | })); 200 | if (player.tank.lastPosition) { 201 | player.tank.position = player.tank.lastPosition; 202 | player.commands = []; 203 | } 204 | }; 205 | do { 206 | collidedPlayers = []; 207 | game.players.forEach(testCollision); 208 | collidedPlayers.forEach(handleCollision); 209 | } while (collidedPlayers.length); 210 | 211 | // Check star 212 | game.players.forEach(function(player, index) { 213 | if (game.star && player.tank.collided({ position: game.star })) { 214 | game.star = null; 215 | game.lastCollectedStar = game.frames; 216 | player.stars += 1; 217 | replay.record({ 218 | type: 'star', 219 | action: 'collected', 220 | by: index 221 | }); 222 | } 223 | }); 224 | 225 | // Move bullets 226 | game.players.forEach(function(player, index) { 227 | if (!player.bullet) { 228 | return; 229 | } 230 | if (player.bullet.collided(game.players[1 - index].tank)) { 231 | replay.record({ 232 | type: 'bullet', 233 | tank: player.tank, 234 | action: 'crashed', 235 | objectId: player.bullet.id 236 | }); 237 | replay.record({ 238 | type: 'tank', 239 | action: 'crashed', 240 | index: index, 241 | objectId: game.players[1 - index].tank.id 242 | }); 243 | game.players[1 - index].tank.crashed = true; 244 | return; 245 | } 246 | for (var i = 0; i < 2; ++i) { 247 | player.bullet.go(); 248 | replay.record({ 249 | type: 'bullet', 250 | tank: player.tank, 251 | action: 'go', 252 | order: i, 253 | position: player.bullet.position.slice(), 254 | objectId: player.bullet.id 255 | }); 256 | if (game.map[player.bullet.position[0]][player.bullet.position[1]] === 'x') { 257 | replay.record({ 258 | type: 'bullet', 259 | tank: player.tank, 260 | action: 'crashed', 261 | objectId: player.bullet.id 262 | }); 263 | player.bullet = null; 264 | break; 265 | } else if (player.bullet.collided(game.players[1 - index].tank)) { 266 | replay.record({ 267 | type: 'bullet', 268 | tank: player.tank, 269 | action: 'crashed', 270 | objectId: player.bullet.id 271 | }); 272 | replay.record({ 273 | type: 'tank', 274 | action: 'crashed', 275 | index: index, 276 | objectId: game.players[1 - index].tank.id 277 | }); 278 | game.players[1 - index].tank.crashed = true; 279 | break; 280 | } 281 | } 282 | }); 283 | 284 | // Listen to the commander when idle 285 | game.players.forEach(function(player, index) { 286 | if (player.pendingCommands.length === 0) { 287 | var commander = new Commander(player.clone()); 288 | var clonedGame = game.clone(); 289 | var enemy = clonedGame.players[1 - index]; 290 | delete clonedGame.players; 291 | // Check if the enemy's bullet is visible 292 | if (enemy.bullet) { 293 | var accessible = false; 294 | var distance, d; 295 | if (enemy.bullet.position[0] === player.tank.position[0] && ( 296 | (enemy.bullet.position[1] > player.tank.position[1] && player.tank.direction === 'down') || 297 | (enemy.bullet.position[1] < player.tank.position[1] && player.tank.direction === 'up') 298 | )) { 299 | accessible = true; 300 | var x = enemy.bullet.position[0]; 301 | distance = enemy.bullet.position[1] - player.tank.position[1]; 302 | for (d = 1; d < Math.abs(distance); ++d) { 303 | if (clonedGame.map[x][player.tank.position[1] + (distance > 0 ? d : -d)] === 'x') { 304 | accessible = false; 305 | break; 306 | } 307 | } 308 | } else if (enemy.bullet.position[1] === player.tank.position[1] && ( 309 | (enemy.bullet.position[0] > player.tank.position[0] && player.tank.direction === 'right') || 310 | (enemy.bullet.position[0] < player.tank.position[0] && player.tank.direction === 'left') 311 | )) { 312 | accessible = true; 313 | var y = enemy.bullet.position[1]; 314 | distance = enemy.bullet.position[0] - player.tank.position[0]; 315 | for (d = 1; d < Math.abs(distance); ++d) { 316 | if (clonedGame.map[player.tank.position[0] + (distance > 0 ? d : -d)][y] === 'x') { 317 | accessible = false; 318 | break; 319 | } 320 | } 321 | } 322 | if (!accessible) { 323 | enemy.bullet = null; 324 | } 325 | } 326 | // Check if the enemy's tank is visible 327 | if (clonedGame.map[enemy.tank.position[0]][enemy.tank.position[1]] === 'o') { 328 | enemy.tank = null; 329 | } 330 | player.onIdle(commander, enemy, clonedGame); 331 | player.pendingCommands = commander.__queue; 332 | } 333 | }); 334 | 335 | if (typeof setImmediate === 'function') { 336 | setImmediate(function() { 337 | update(callback); 338 | }); 339 | } else { 340 | setTimeout(function() { 341 | update(callback); 342 | }, 0); 343 | } 344 | } 345 | 346 | update(function(err, gameReplay) { 347 | callback(err, gameReplay); 348 | }); 349 | }; 350 | -------------------------------------------------------------------------------- /services/calc_rank.js: -------------------------------------------------------------------------------- 1 | if (require.main === module) { 2 | require('../env'); 3 | } 4 | 5 | var async = require('async'); 6 | var aqsort = require('aqsort'); 7 | 8 | var runCodes = require('./util'); 9 | 10 | var MAP_ID; 11 | var round = 0; 12 | 13 | var calc = module.exports = function(end) { 14 | var startTime = new Date(); 15 | Code.findAll().done(function(err, codeResult) { 16 | codeResult = codeResult.filter(function(item) { 17 | return item.code.length > 700; 18 | }); 19 | console.log('Valid codes: ' + codeResult.length); 20 | aqsort(codeResult.map(function(item) { 21 | item = item.dataValues; 22 | item.win = item.lose = 0; 23 | item.winReasons = {}; 24 | item.loseReasons = {}; 25 | return item; 26 | }), function(a, b, callback) { 27 | round += 1; 28 | runCodes(MAP_ID, a, b, callback); 29 | }, function(err, result) { 30 | var C = []; 31 | var top = result.splice(0, 30); 32 | top.forEach(function(item) { 33 | item.win = item.lose = 0; 34 | item.winReasons = {}; 35 | item.loseReasons = {}; 36 | }); 37 | var min = top.length > 30 ? 30 : top.length; 38 | for (var i = 0; i < min; ++i) { 39 | for (var j = i + 1; j < min; ++j) { 40 | C.push([i, j]); 41 | } 42 | } 43 | async.eachSeries(C, function(item, next) { 44 | runCodes(MAP_ID, top[item[0]], top[item[1]], next); 45 | }, function() { 46 | result = top.sort(function(a, b) { 47 | return b.win - a.win; 48 | }).concat(result); 49 | result.forEach(function(item, index) { 50 | item.rank = index + 1; 51 | item.winReason = maxReason(item.winReasons); 52 | item.loseReason = maxReason(item.loseReasons); 53 | }); 54 | Code.update({ 55 | rank: null, 56 | win: 0, 57 | lost: 0, 58 | winReason: '', 59 | loseReason: '' 60 | }, { where: {} }).done(function() { 61 | async.eachLimit(result, 10, function(item, next) { 62 | Code.update({ 63 | rank: item.rank, 64 | win: item.win, 65 | lost: item.lose, 66 | winReason: item.winReason, 67 | loseReason: item.loseReason, 68 | }, { 69 | where: { UserId: item.UserId } 70 | }).done(next); 71 | }, function() { 72 | console.log('Done(' + round + '): ' + (new Date() - startTime)); 73 | if (end) { 74 | end(); 75 | } 76 | }); 77 | }); 78 | }); 79 | }); 80 | // }); 81 | }); 82 | }; 83 | 84 | function maxReason(reasons) { 85 | var max = { n: 0, v: '' }; 86 | Object.keys(reasons).forEach(function(reason) { 87 | if (reasons[reason] > max.n) { 88 | max.n = reasons[reason]; 89 | max.v = reason; 90 | } 91 | }); 92 | return max.v; 93 | } 94 | 95 | if (require.main === module) { 96 | Map.find({ where: { type: 'rank' } }).then(function(map) { 97 | MAP_ID = map.id; 98 | calc(function() { 99 | process.exit(0); 100 | }); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /services/calc_tournament.js: -------------------------------------------------------------------------------- 1 | if (require.main === module) { 2 | require('../env'); 3 | } 4 | 5 | var async = require('async'); 6 | var tournamentId = process.argv[2]; 7 | 8 | var isend = false; 9 | 10 | function calcFull(current) { 11 | var base = 1; 12 | while (current > base) { 13 | base *= 2; 14 | } 15 | return base; 16 | } 17 | 18 | function runCodes(maps, users, code, callback) { 19 | var score = [0, 0]; 20 | async.mapSeries(maps, function(map, next) { 21 | Game(map.id, code[0].code, code[1].code, function(err, replay, _, result) { 22 | next(err, { replay: replay, resultId: result.id }); 23 | }); 24 | }, function(err, result) { 25 | result.forEach(function(r) { 26 | score[r.replay.meta.result.winner] += 1; 27 | }); 28 | score.push({ 29 | id: ++UID, 30 | users: users.map(function(user) { 31 | return { 32 | id: user.id, 33 | login: user.login, 34 | name: user.name 35 | }; 36 | }), 37 | maps: maps.map(function(map) { return map.id; }), 38 | result: result.map(function(r) { return r.resultId; }) 39 | }); 40 | callback(null, score); 41 | }); 42 | } 43 | 44 | var UID = 0; 45 | 46 | var position3and4 = []; 47 | Tournament.find({ 48 | where: { id: tournamentId }, 49 | include: [{ model: User }, { model: Map }] 50 | }).then(function(tournament) { 51 | Code.findAll({ 52 | where: { UserId: tournament.Users.map(function(user) { return user.id; }) } 53 | }).then(function(codes) { 54 | var result = { 55 | teams: [], 56 | results: [] 57 | }; 58 | var round = []; 59 | var players = tournament.Users.filter(function(user) { 60 | return codes.filter(function(code) { 61 | return code.UserId === user.id; 62 | }).length; 63 | }).sort(function(a, b) { 64 | var codea = codes.filter(function(code) { 65 | return code.UserId === a.id; 66 | })[0]; 67 | var codeb = codes.filter(function(code) { 68 | return code.UserId === b.id; 69 | })[0]; 70 | return (codea.rank || Number.MAX_VALUE) - (codeb.rank || Number.MAX_VALUE); 71 | }); 72 | var allCount = calcFull(players.length); 73 | var diff = allCount - players.length; 74 | for (var i = 0; i < allCount; i += 2) { 75 | if (diff) { 76 | if (players.length >= 1) { 77 | round.push([players.shift(), '空缺']); 78 | diff -= 1; 79 | } else { 80 | round.push(['空缺', '空缺']); 81 | diff -= 2; 82 | } 83 | } else { 84 | if (players.length >= 2) { 85 | round.push([players.shift(), players.shift()]); 86 | } else if (players.length >= 1) { 87 | round.push([players.shift(), '空缺']); 88 | } else { 89 | round.push(['空缺', '空缺']); 90 | } 91 | } 92 | } 93 | var newRound = []; 94 | var length = round.length; 95 | while (round.length) { 96 | newRound.push(round.shift()); 97 | newRound.push(round.pop()); 98 | } 99 | round = newRound; 100 | round.forEach(function(couple) { 101 | result.teams.push(couple.map(function(user) { 102 | if (user === '空缺') { 103 | return user; 104 | } 105 | return { id: user.id, login: user.login, name: user.name }; 106 | })); 107 | }); 108 | async.whilst(function() { 109 | return round.length !== 0; 110 | }, function(callback) { 111 | console.log('Round: ' + round.length); 112 | async.mapSeries(round, function(item, next) { 113 | if (item[1] === '空缺') { 114 | return next(null, [tournament.Maps.length, 0]); 115 | } 116 | var code = item.map(function(user) { 117 | return codes.filter(function(code) { 118 | return user.id === code.UserId; 119 | })[0]; 120 | }); 121 | runCodes(tournament.Maps, item, code, next); 122 | }, function(err, scores) { 123 | result.results.push(scores); 124 | var newRound = []; 125 | if (round.length === 2 && !isend) { 126 | isend = true; 127 | var win = round.map(function(r, index) { 128 | if (scores[index][0] > scores[index][1]) { 129 | return r[0]; 130 | } else { 131 | return r[1]; 132 | } 133 | }); 134 | var lose = round.map(function(r, index) { 135 | if (scores[index][0] < scores[index][1]) { 136 | return r[0]; 137 | } else { 138 | return r[1]; 139 | } 140 | }); 141 | round = [win, lose]; 142 | callback(); 143 | return; 144 | } 145 | round = round.map(function(r, index) { 146 | if (scores[index][0] > scores[index][1]) { 147 | return r[0]; 148 | } else { 149 | return r[1]; 150 | } 151 | }); 152 | round.forEach(function(player, index) { 153 | if (index % 2) { 154 | newRound.push([round[index - 1], player]); 155 | } 156 | }); 157 | round = newRound; 158 | callback(); 159 | }); 160 | }, function() { 161 | tournament.result = JSON.stringify(result); 162 | tournament.save().then(function() { 163 | process.exit(0); 164 | }); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /services/util.js: -------------------------------------------------------------------------------- 1 | module.exports = function(map, a, b, callback) { 2 | process.stdout.write(a.UserId + '\t' + b.UserId + '\t'); 3 | var start = Date.now(); 4 | Game(map, a.code, b.code, function(err, replay) { 5 | var winner, loser; 6 | var result; 7 | if (replay.meta.result.winner === 0) { 8 | winner = a; 9 | loser = b; 10 | result = -1; 11 | } else { 12 | winner = b; 13 | loser = a; 14 | result = 1; 15 | } 16 | winner.win += 1; 17 | loser.lose += 1; 18 | var reason = replay.meta.result.reason; 19 | if (typeof winner.winReasons[reason] === 'undefined') { 20 | winner.winReasons[reason] = 1; 21 | } else { 22 | winner.winReasons[reason] += 1; 23 | } 24 | if (typeof loser.loseReasons[reason] === 'undefined') { 25 | loser.loseReasons[reason] = 1; 26 | } else { 27 | loser.loseReasons[reason] += 1; 28 | } 29 | process.stdout.write((result === -1 ? 'win' : 'lost') + '\t' + (Date.now() - start) + 'ms\n'); 30 | process.nextTick(function() { 31 | callback(null, result); 32 | }); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /views/doc/api.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 | a(href="/doc/tournament") 杯赛规则 12 | li AI 编写文档 13 | article 14 | h2 编写语言 15 | p Code Game 的 AI 脚本(以下简称“脚本”)使用 JavaScript(ES 5) 编写。 16 | 17 | h2 运行环境 18 | p 所有脚本均运行与后端沙盒服务器,不会被其他玩家所见。 19 | 20 | h2 接口指南 21 | span 了解如何编写一个脚本 22 | 23 | p 每个脚本均需要实现 onIdle 函数,在游戏运行过程中,一旦脚本所控制的坦克处于空闲状态,游戏引擎就会调用脚本的 onIdle 函数,并将游戏的状态(如地图信息,坦克、炮弹和星星的位置,当前帧数)传递给该函数。在 onIdle 函数中,开发者需要做的就是根据游戏的状态向自己控制的坦克下达指令(前进、转向和开火)。当坦克执行完所有操作后,游戏引擎会再次请求 onIdle 函数,从而形成循环。 24 | p 游戏引擎会传递给 onIdle 3个参数,分别为 me, enemygame,下面分别介绍。 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.tanknull,当敌方子弹不可见时,enemy.bulletnull。 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 | --------------------------------------------------------------------------------