121 Commits

Author SHA1 Message Date
ebd88fe643 Don't need the FPS setting (not used in code) 2024-09-29 11:48:55 -07:00
5927290ba8 Use proper link/icon 2024-09-29 11:34:01 -07:00
8a76347ca6 Increase grid size to 128 [PR]
Reviewed-on: #1
2024-09-29 14:29:24 -04:00
4037350e7e 128 2024-09-29 14:00:38 -04:00
bf6e29bc1b NEVER MIND REMOVING TAIL COLLISION WAS BAD IDEA 2024-09-29 09:44:06 -07:00
cbe189bbed Update config 2024-09-29 09:40:40 -07:00
5d7c979752 Border freezes player in position, not kill 2024-09-29 09:40:15 -07:00
04a365fbc9 Tails do not kill anymore 2024-09-29 09:39:39 -07:00
39421c43b5 Change logo 2024-09-29 08:44:46 -07:00
518a1f3ef8 Update gitingore 2024-09-29 08:43:51 -07:00
Mimi
5b8d347bae Update dependencies 2022-11-12 20:44:53 +08:00
Mimi
69ed3e56e2 Fix kills 2022-11-12 20:38:09 +08:00
Mimi
7044b67cb0 Support different socket.io for client and server 2022-11-12 20:26:51 +08:00
Mimi
fbf7ff07d4 Use ESM 2022-11-12 20:26:51 +08:00
Mimi
0104b6d766 Update dependencies 2021-04-26 23:25:18 +08:00
Mimi
4946fe70cc Code style update 2020-12-09 23:45:58 +08:00
Mimi
b28d535903 Upgrade socket.io 2020-11-26 23:12:54 +08:00
Mimi
3b3c8929e1 Use mimi-server 2020-11-25 00:26:49 +08:00
Mimi
3d10dd527c Class syntax 2020-07-24 15:22:41 +08:00
Mimi
01da4d3e55 Run lebab 2020-07-24 15:11:35 +08:00
Mimi
b8b22ea3d4 Use fork 2020-07-22 20:56:04 +08:00
Mimi
60b5b98033 Optimize style 2020-04-16 18:56:54 +08:00
Mimi
b9398212dc Install font-awesome using npm 2020-04-16 18:44:34 +08:00
Mimi
342c50a995 Remove jQuery file 2020-03-06 21:34:27 +08:00
Mimi
296c3d116a Use watchify 2020-03-05 11:54:21 +08:00
Mimi
a572afebc7 Install jQuery via npm 2020-03-04 20:25:34 +08:00
Mimi
a40cba4e5f Remove bundle.js 2020-03-04 20:13:16 +08:00
Mimi
0005563b46 Use arrow functions 2020-03-04 20:07:38 +08:00
Mimi
c44446eb65 Update dependencies 2020-03-04 20:03:59 +08:00
Mimi
2244a6bb5d Replace destructuring assignment 2020-03-04 18:53:32 +08:00
Mimi
8ab91871ff Use chalk 2020-02-04 11:10:32 +08:00
Mimi
efd60e662a Use event.key 2020-02-04 11:08:01 +08:00
Mimi
a2cc7a92c3 Switch to LF 2020-01-30 18:09:49 +08:00
Mimi
d6918c06d7 Use ES6 syntax 2020-01-30 18:09:07 +08:00
Mimi
182d3a1d76 Replace uglify-es with terser 2020-01-28 19:46:14 +08:00
Mimi
ecf25f65a2 Build 2019-12-01 10:52:56 +08:00
Mimi
fe940a3182 Use location.host 2019-11-30 16:58:13 +08:00
Mimi
277220ae14 Use LF instead of CRLF 2019-11-30 13:25:14 +08:00
Mimi
a8a0ed36a9 Use location.protocol 2019-11-30 13:24:45 +08:00
Mimi
539a370161 Use arrow function 2019-11-27 20:52:39 +08:00
Mimi
267a787387 Update docs 2019-10-14 00:57:04 +08:00
StevenJoeZhang
8c1ad1aaa3 Update dependencies 2019-09-26 18:49:18 +08:00
StevenJoeZhang
1ffd662600 Code style update 2019-08-01 20:27:05 +08:00
StevenJoeZhang
1837919ee4 Update dependencies 2019-07-30 08:20:26 +08:00
StevenJoeZhang
459c341be5 Code format 2019-05-23 16:20:12 +08:00
StevenJoeZhang
4f53513e4b Update 2019-05-23 16:20:08 +08:00
StevenJoeZhang
d4f45d250b Rename files 2019-05-23 16:20:01 +08:00
StevenJoeZhang
71075a53cf Update README.md 2019-05-23 16:19:59 +08:00
StevenJoeZhang
0abbd33cfa Improve 2019-05-23 16:19:55 +08:00
StevenJoeZhang
a9b9e8e895 Use express instead of finalhandler & serve-static 2019-05-23 16:19:54 +08:00
StevenJoeZhang
28e8f611f2 Update packages 2019-05-23 16:19:54 +08:00
StevenJoeZhang
b811b7d96f Settings 2019-05-23 16:19:48 +08:00
StevenJoeZhang
a3b2d660a7 rename files & add font-awesome 2019-02-22 18:44:15 +08:00
StevenJoeZhang
728343ea0d improve style 2019-02-22 16:09:27 +08:00
StevenJoeZhang
33f6dbd00e optimize canvas style 2019-02-22 15:10:20 +08:00
StevenJoeZhang
afa909f9e2 add mode-god 2019-02-22 13:24:01 +08:00
StevenJoeZhang
d3aed9e5a5 update 2019-02-22 13:24:01 +08:00
StevenJoeZhang
73958b9252 format code 2019-02-22 13:24:01 +08:00
StevenJoeZhang
df1dfa3062 rename GRID_SIZE -> GRID_COUNT 2019-02-22 13:23:58 +08:00
StevenJoeZhang
2cf9221ac2 rename files 2019-02-22 01:08:40 +08:00
StevenJoeZhang
32449da6fb move consts to config.json 2019-02-22 00:52:30 +08:00
StevenJoeZhang
dad3583c68 use config.json 2019-02-22 00:19:33 +08:00
StevenJoeZhang
a7d298f625 Const names 2019-02-03 14:49:23 +08:00
StevenJoeZhang
bb6cb3171e Rename files 2019-01-16 18:04:24 +08:00
StevenJoeZhang
5c72a93605 Username 2019-01-16 16:00:38 +08:00
StevenJoeZhang
2ea6801dcd Touch support 2019-01-16 13:28:35 +08:00
StevenJoeZhang
b535f5c354 Format code 2019-01-16 11:07:52 +08:00
Henry Wang
c9e8554d8c Merge pull request #11 from MrRefactoring/added_WASD_support
Added WASD support
2018-11-23 15:22:17 -06:00
MrRefactoring
ed01c8a8ef added WASD support 2018-11-22 15:17:22 +03:00
Henry Wang
c0869b47c5 Update README 2017-12-29 00:58:33 -06:00
Henry Wang
783a06b63d Remove unnecessary packages 2017-11-21 13:08:13 -06:00
Henry Wang
57f28e279c Update coefficients from testing 2017-11-21 02:25:21 +00:00
Henry Wang
a1887b2b7d Fix paper-io bot
* Reset the movement array as soon as it hits it's land
2017-11-20 20:21:52 -06:00
Henry Wang
2bea2e8b7a Update bot mode, add paper-io bot 2017-11-17 01:49:49 -06:00
Henry Wang
ebc5e93ae5 Update dependencies 2017-11-17 01:38:11 +00:00
Henry Wang
73bf9f86f2 Reconnect bot if error occurs 2017-11-14 12:54:11 -06:00
Henry Wang
64d9c5e6bc Ignore BOTs 2017-11-03 18:24:43 -05:00
Henry Wang
d83ec69553 First generation of bots! 2017-11-03 18:05:50 -05:00
Henry Wang
efd5f58b9e Fix some node paths 2017-10-30 23:06:50 -05:00
Henry Wang
1bb7997879 Add package lock file 2017-10-31 03:38:56 +00:00
Henry Wang
0df331b04a Modify color generating algo and update logging 2017-10-30 22:11:38 -05:00
Henry Wang
ca82979c7c Update gitignore 2017-10-31 02:50:17 +00:00
Henry Wang
be477f780e Remove npm-debug.log 2017-10-30 21:25:54 -05:00
Henry Wang
2ddfc7c0a0 Modularize Node.js project 2017-10-30 21:24:39 -05:00
Henry Wang
d7f1af47e2 Fix global var issue 2017-09-04 17:59:35 -05:00
Henry Wang
454fc98865 Remove frame verification 2017-09-04 14:33:29 -05:00
Henry Wang
e78c78cefc Remove frame verification 2017-09-04 14:32:26 -05:00
Henry Wang
893a19dbd8 Fix error 2017-09-04 13:54:30 -05:00
Henry Wang
870c939bcc Fix bot code 2017-09-04 12:23:12 -05:00
theKidOfArcrania
858b8f0434 Add bot stub 2017-09-04 16:53:37 +00:00
theKidOfArcrania
c8cd3d474b Separate rendering and client code, fixes #3 2017-09-04 16:27:08 +00:00
Henry Wang
7ff47d8efc Game size adjusts to window 2017-04-17 16:14:25 -05:00
theKidOfArcrania
8d50dc1d54 Update playground link 2017-04-17 13:04:22 -05:00
theKidOfArcrania
075b55d5ec Fix some rendering stuff 2017-03-12 18:47:14 +00:00
theKidOfArcrania
f83ca3dc04 Add stats screen, change name to Blockly.IO 2017-03-04 12:44:27 +00:00
Henry Wang
c18d656eb1 Add name restriction 2017-03-03 15:52:59 -06:00
Henry Wang
d86454ffc4 Copy static files to local server 2017-03-02 18:27:55 -06:00
Henry Wang
57efbb5837 Use correct host in game client 2017-03-02 18:00:09 -06:00
Henry Wang
c65cfe14b2 Add changing host options 2017-03-02 17:36:11 -06:00
theKidOfArcrania
01b0b5833c Have the leaderboard bars roll animate up and down 2017-03-02 22:38:29 +00:00
theKidOfArcrania
6949374b8e Check if we can connect with WS 2017-03-02 19:02:03 +00:00
theKidOfArcrania
fab29a8f75 UI impls and bug fixes.
* Implement leaderboard (shows top five players).
* Make color be decided on server side (make sure we reuse the correct colors)
* Better cache frames during 'requestFrames' mechanism
* Make sure we die when we disconnect.
* Make sure that new client doesn't consider all players as new clients (remove newPlayerFrames, and use Player.waitLag instead)
* Have default player name be empty (or "Unnamed")
* Fix refresh issue when new player joins game.
2017-03-02 06:49:46 +00:00
theKidOfArcrania
8352433fa5 Optimize flood-fill and fix OBOB + player bug
The flood-fill had allocated a good amount of memory ~1MB each time we visit a square. This is reduced to the grid size squared, only allocated if the square we are on now is valid.

The OBOB error occured when server was updating one time too few when a player is initialized.

The player bug occurs when a new player joins the game. Normally each new player gets about 60ish frames of lag to start up. However, the new player client incorrectly assumes all other players are new as well.
2017-03-01 00:56:56 +00:00
theKidOfArcrania
e8fd5d1f7c Fix some bugs, start up screen 2017-02-28 07:21:27 +00:00
theKidOfArcrania
cb1740c117 Add running instructions. 2017-02-26 18:26:22 -06:00
theKidOfArcrania
87054842de Don't repaint unnecessary frames (when we lag) 2017-02-27 00:23:02 +00:00
theKidOfArcrania
bdfc2984ee Fix a race condition 2017-02-26 23:59:49 +00:00
theKidOfArcrania
134d4e4cc0 Fix OBOE error :( And tidy up code 2017-02-26 23:01:57 +00:00
Henry Wang
6bc7ef86cc Server now works! (Fix two bugs) 2017-02-26 01:31:45 -06:00
Henry Wang
f902663904 Slow down server. Remove node_modules 2017-02-25 22:25:06 -06:00
theKidOfArcrania
88a6195fe8 Server and client code completed (with bugs). 2017-02-26 02:36:44 +00:00
theKidOfArcrania
916c50e162 Server logic for each game 2017-02-25 23:52:52 +00:00
Henry Wang
52dc9a068f Show rank. 2017-02-25 00:09:34 -06:00
Henry Wang
209e4da6f9 Fix some collision and rendering bugs 2017-02-24 23:33:34 -06:00
Henry Wang
777a76e94e Zoom now works. 2017-02-24 22:59:05 -06:00
Henry Wang
7e6f2505b4 Zoom depending on player's area. Not done 2017-02-24 18:57:13 -06:00
Henry Wang
4a0bd33a44 UI Changes 2017-02-24 01:17:49 -06:00
Henry Wang
6e9377387d Add socket.io 2017-02-24 00:49:41 -06:00
theKidOfArcrania
370d13cdc8 Update README.md 2017-02-23 22:38:52 -06:00
theKidOfArcrania
6a793007a3 Create LICENSE 2017-02-23 22:37:07 -06:00
theKidOfArcrania
d4c82cbff2 Create README.md 2017-02-23 22:36:15 -06:00
33 changed files with 3074 additions and 1400 deletions

95
.gitignore vendored Normal file
View File

@@ -0,0 +1,95 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Npm Lock files
package-lock.json
# Dist
dist/
# Backups and vim swp files
*~
*.sw?
.DS_Store
paper-io.log
public/js/bundle.js

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 theKidOfArcrania
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
README.md Normal file
View File

@@ -0,0 +1,67 @@
# Paper.IO
> This is a clone of the original Paper-IO released by Voodoo, except for one aspect. This will attempt to implement a multi-player aspect of the game (like a real IO game). Currently this has a playground at this [link](https://thekidofarcrania.github.io/BlocklyIO). It's a demo version of what to come. Hopefully by that time, the necessary server infrastructure could be obtained.
> This is just a fun side-project for me. If you would want to use this code, it would be nice to let me know.
## Screenshots
![Screenshot](screenshot.png)
## Install
```bash
# Clone this repository
git clone https://github.com/stevenjoezhang/paper.io.git
# Go into the repository
cd paper.io
# Install dependencies
npm install
```
## Usage
After cloning this repository, run the follow commands to install dependencies and set up server. Enjoy!
```bash
npm start
```
You can configure the game by editing `config.js`.
## Build
```bash
npm run build
```
**WARNING: Remember to build again after editing any file, include `config.js`.**
## Bots
Set `bots` in `config.js` to a non-zero value, or execute the command below:
```bash
node paper-io-bot.js ws://localhost:8080
```
or
```bash
node bot.js ws://localhost:8080
```
## Roadmap & TODO List
- [x] 统一配置文件
- [x] 玩家观战模式
- [ ] 更多游戏玩法
- [ ] 多个游戏房间
- [ ] 加快渲染速度
- [ ] 优化胜负判定
## License
This repo is forked from [BlocklyIO](https://github.com/theKidOfArcrania/BlocklyIO) by theKidOfArcrania.
> This is licensed under MIT. As such, please provide due credit and link back to this repository if possible.

284
bot.js Normal file
View File

@@ -0,0 +1,284 @@
if (process.argv.length < 3) {
console.log("Usage: node bot.js <socket-url> [<name>]")
process.exit(1);
}
//TODO: add a land claiming algo (with coefficient parameters)
//TODO: add weight to the max land area and last land area, and also the number of kills
//TODO: genetic gene pooling
import { Grid } from "./src/core";
import client from "./src/game-client";
import { consts } from "./config.js";
const MOVES = [[-1, 0], [0, 1], [1, 0], [0, -1]];
const AGGRESSIVE = Math.random();
const THRESHOLD = 10;
let startFrame = -1;
let endFrame = -1;
const coeffs = [0.6164220147940495, -2.519369747858328, 0.9198978109542851, -1.2158956330674564, -3.072901620397528, 5, 4];
let grid;
let others;
let user;
const playerPortion = {};
const DIST_TYPES = {
land: {
check: function(loc) {
return grid.get(loc.row, loc.col) === user;
},
coeff: function() {
return coeffs[0];
}
}, tail: {
check: function(loc) {
return tail(user, loc)
},
coeff: function() {
return coeffs[1];
}
}, oTail: {
check: foundProto(tail),
coeff: function() {
return AGGRESSIVE * coeffs[2];
}
}, other: {
check: foundProto(function(other, loc) {
return other.row === this.row && other.col === this.col;
}),
coeff: function() {
return (1 - AGGRESSIVE) * coeffs[3];
}
}, edge: {
check: function(loc) {
return loc.row <= 1 || loc.col <= 1 || loc.row >= consts.GRID_COUNT - 1 || loc.col >= consts.GRID_COUNT - 1
},
coeff: function() {
return coeffs[4];
}
}
};
function generateLandDirections() {
function mod(x) {
x %= 4;
if (x < 0) x += 4;
return x;
}
const breadth = Math.floor(Math.random() * coeffs[5]) + 1;
const spread = Math.floor(Math.random() * coeffs[6]) + 1;
const extra = Math.floor(Math.random() * 2) + 1;
const ccw = Math.floor(Math.random() * 2) * 2 - 1;
const dir = user.currentHeading;
const turns = [dir, mod(dir + ccw), mod(dir + ccw * 2), mod(dir + ccw * 3)];
const lengths = [breadth, spread, breadth + extra, spread];
const moves = [];
for (let i = 0; i < turns.length; i++) {
for (let j = 0; j < lengths[i]; j++) {
moves.push(turns[i]);
}
}
}
const LAND_CLAIMS = {
rectDims: function() {},
rectSpread: function() {}
};
function foundProto(func) {
return loc => {
return others.some(other => {
return func(other, loc);
});
};
}
function connect() {
const prefixes = consts.PREFIXES.split(" ");
const names = consts.NAMES.split(" ");
const name = process.argv[3] || [prefixes[Math.floor(Math.random() * prefixes.length)], names[Math.floor(Math.random() * names.length)]].join(" ");
client.connectGame(process.argv[2], "[BOT] " + name, (success, msg) => {
if (!success) {
console.error(msg);
setTimeout(connect, 1000);
}
});
}
function Loc(row, col, step) {
if (this.constructor != Loc) return new Loc(row, col, step);
this.row = row;
this.col = col;
this.step = step;
}
//Projects vector b onto vector a
function project(a, b) {
const factor = (b[0] * a[0] + b[1] * a[1]) / (a[0] * a[0] + a[1] * a[1]);
return [factor * a[0], factor * a[1]];
}
function tail(player, loc) {
return player.tail.hitsTail(loc);
}
function traverseGrid(dir) {
steps = new Array(consts.GRID_COUNT * consts.GRID_COUNT);
for (let i in steps) {
steps[i] = -1;
}
distWeights = {};
for (const type in DIST_TYPES) {
distWeights[type] = 0;
}
const { row, col } = user;
const minRow = Math.max(0, row - 10), maxRow = Math.min(consts.GRID_COUNT, row + 10);
const minCol = Math.max(0, col - 10), maxCol = Math.min(consts.GRID_COUNT, col + 10);
let proj = 0;
for (let i = 1; i >= -1; i-=2) {
proj = (1 + THRESHOLD) * i;
while (proj != 0) {
proj -= i;
const normRange = Math.abs(proj);
for (let norm = -normRange; norm <= normRange; norm++) {
for (const distType in distWeights) {
const move = MOVES[dir];
const delta = THRESHOLD - Math.abs(proj);
const dist = Math.sign(proj) * delta * delta / (Math.abs(norm) + 1);
const loc = {row: proj * move[0] + norm * move[1], col: proj * move[1] + norm * move[0]};
loc.row += user.row;
loc.col += user.col;
if (loc.row < 0 || loc.row >= consts.GRID_COUNT || loc.col < 0 || loc.col >= consts.GRID_COUNT) continue;
if (DIST_TYPES[distType].check(loc)) distWeights[distType] += dist;
}
}
}
}
return distWeights;
}
function printGrid() {
const chars = new Grid(consts.GRID_COUNT);
for (let r = 0; r < consts.GRID_COUNT; r++) {
for (let c = 0; c < consts.GRID_COUNT; c++) {
if (tail(user, {row: r, col: c})) chars.set(r, c, "t");
else {
const owner = grid.get(r, c);
chars.set(r, c, owner ? "" + owner.num % 10 : ".");
}
}
}
for (const p of others) {
chars.set(p.row, p.col, "x");
}
chars.set(user.row, user.col, "^>V<"[user.currentHeading]);
let str = "";
for (let r = 0; r < consts.GRID_COUNT; r++) {
str += "\n";
for (let c = 0; c < consts.GRID_COUNT; c++) {
str += chars.get(r, c);
}
}
console.log(str);
}
function update(frame) {
if (startFrame == -1) startFrame = frame;
endFrame = frame;
if (frame % 6 == 1) {
grid = client.grid;
others = client.getOthers();
//printGrid();
const weights = [0, 0, 0, 0];
for (let d of [3, 0, 1]) {
let weight = 0;
d = (d + user.currentHeading) % 4;
distWeights = traverseGrid(d);
let str = d + ": ";
for (const distType in DIST_TYPES) {
const point = distWeights[distType] * DIST_TYPES[distType].coeff();
weight += point;
str += distType + ": " + point + ", ";
}
//console.log(str);
weights[d] = weight;
}
const low = Math.min(0, Math.min.apply(this, weights));
let total = 0;
weights[(user.currentHeading + 2) % 4] = low;
for (let i = 0; i < weights.length; i++) {
weights[i] -= low * (1 + Math.random());
total += weights[i];
}
if (total == 0) {
for (let d of [-1, 0, 1]) {
d = (d + user.currentHeading) % 4;
while (d < 0) d += 4;
weights[d] = 1;
total++;
}
}
//console.log(weights)
//Choose a random direction from the weighted list
let choice = Math.random() * total;
let d = 0;
while (choice > weights[d]) {
choice -= weights[d++];
}
client.changeHeading(d);
}
}
function calcFavorability(params) {
return params.portion + params.kills * 50 + params.survival / 100;
}
client.setAllowAnimation(false);
client.setRenderer({
addPlayer: function(player) {
playerPortion[player.num] = 0;
},
disconnect: function() {
const dt = (endFrame - startFrame);
startFrame = -1;
console.log(`[${new Date()}] I died... (survived for ${dt} frames.)`);
console.log(`[${new Date()}] I killed ${client.getKills()} player(s).`);
console.log("Coefficients: " + coeffs);
const mutation = Math.min(10, Math.pow(2, calcFavorability(params)));
for (let i = 0; i < coeffs.length; i++) {
coeffs[i] += Math.random() * mutation * 2 - mutation;
}
connect();
},
removePlayer: function(player) {
delete playerPortion[player.num];
},
setUser: function(u) {
user = u;
},
update: update,
updateGrid: function(row, col, before, after) {
before && playerPortion[before.num]--;
after && playerPortion[after.num]++;
}
});
connect();

1373
bundle.js

File diff suppressed because it is too large Load Diff

103
client.js Normal file
View File

@@ -0,0 +1,103 @@
import jquery from "jquery";
import io from "socket.io-client/dist/socket.io.js";
import * as client from "./src/game-client";
import godRenderer from "./src/mode/god";
import * as playerRenderer from "./src/mode/player";
const $ = jquery;
function run(flag) {
client.setRenderer(flag ? godRenderer : playerRenderer);
client.connectGame(io, "//" + location.host, $("#name").val(), (success, msg) => {
if (success) {
$("#main-ui").fadeIn(1000);
$("#begin, #wasted").fadeOut(1000);
}
else {
$("#error").text(msg);
}
}, flag);
}
$(() => {
const err = $("#error");
if (!window.WebSocket) {
err.text("Your browser does not support WebSockets!");
return;
}
err.text("Loading... Please wait"); //TODO: show loading screen
(() => {
const socket = io(`//${location.host}`, {
forceNew: true,
upgrade: false,
transports: ["websocket"]
});
socket.on("connect", () => {
socket.emit("pings");
});
socket.on("pongs", () => {
socket.disconnect();
err.text("All done, have fun!");
$("#name").on("keypress", evt => {
if (evt.key === "Enter") run();
});
$(".start").removeAttr("disabled").on("click", evt => {
run();
});
$(".spectate").removeAttr("disabled").click(evt => {
run(true);
});
});
socket.on("connect_error", () => {
err.text("Cannot connect with server. This probably is due to misconfigured proxy server. (Try using a different browser)");
});
})();
});
//Event listeners
$(document).on("keydown", e => {
let newHeading = -1;
switch (e.key) {
case "w": case "ArrowUp":
newHeading = 0; break; //UP (W)
case "d": case "ArrowRight":
newHeading = 1; break; //RIGHT (D)
case "s": case "ArrowDown":
newHeading = 2; break; //DOWN (S)
case "a": case "ArrowLeft":
newHeading = 3; break; //LEFT (A)
default: return; //Exit handler for other keys
}
client.changeHeading(newHeading);
//e.preventDefault();
});
$(document).on("touchmove", e => {
e.preventDefault();
});
$(document).on("touchstart", e1 => {
const x1 = e1.targetTouches[0].pageX;
const y1 = e1.targetTouches[0].pageY;
$(document).one("touchend", e2 => {
const x2 = e2.changedTouches[0].pageX;
const y2 = e2.changedTouches[0].pageY;
const deltaX = x2 - x1;
const deltaY = y2 - y1;
let newHeading = -1;
if (deltaY < 0 && Math.abs(deltaY) > Math.abs(deltaX)) newHeading = 0;
else if (deltaX > 0 && Math.abs(deltaY) < deltaX) newHeading = 1;
else if (deltaY > 0 && Math.abs(deltaX) < deltaY) newHeading = 2;
else if (deltaX < 0 && Math.abs(deltaX) > Math.abs(deltaY)) newHeading = 3;
client.changeHeading(newHeading);
});
});
$(".menu").on("click", () => {
client.disconnect();
$("#main-ui, #wasted").fadeOut(1000);
$("#begin").fadeIn(1000);
});
$(".toggle").on("click", () => {
$("#settings").slideToggle();
});

18
config.js Normal file
View File

@@ -0,0 +1,18 @@
export const config = {
"dev": true,
"port": 62787,
"bots": 0,
};
export const consts = {
"GRID_COUNT": 128,
"CELL_WIDTH": 40,
"SPEED": 5,
"BORDER_WIDTH": 20,
"MAX_PLAYERS": 30,
"NEW_PLAYER_LAG": 60,
"LEADERBOARD_NUM": 5,
"PREFIXES": "Cute Adorable Scratchy Angry",
"NAMES": "Cat Kitty Kitten Feline"
};

View File

@@ -1,27 +0,0 @@
<head>
<link href="https://fonts.googleapis.com/css?family=Changa:600" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="bundle.js"></script>
<style>
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: black;
}
canvas {
image-rendering: optimizeSpeed; /* Older versions of FF */
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
image-rendering: -webkit-optimize-contrast; /* Safari */
image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */
image-rendering: pixelated; /* Awesome future-browsers */
-ms-interpolation-mode: nearest-neighbor; /* IE */
}
</style>
</head>
<body>
<canvas id="main-ui"></canvas>
</body>

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "paper-io",
"version": "0.0.1",
"private": true,
"description": "An multiplayer-IO type game (cloned from Paper-IO)",
"main": "server.js",
"type": "module",
"scripts": {
"build": "rollup -c rollup.config.js -f iife | terser -c -m > public/js/bundle.js",
"build-dev": "rollup -c rollup.config.js -f iife -o public/js/bundle.js -w"
},
"repository": "stevenjoezhang/paper.io",
"keywords": [
"Paper-IO",
"IO",
"Game"
],
"author": "Mimi <stevenjoezhang@gmail.com> (https://zhangshuqiao.org)",
"license": "MIT",
"bugs": {
"url": "https://github.com/stevenjoezhang/paper.io/issues"
},
"homepage": "https://github.com/stevenjoezhang/paper.io",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.2.0",
"jquery": "^3.6.1",
"mimi-server": "^0.0.1",
"socket.io": "^4.5.3",
"socket.io-client": "^4.5.3"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"rollup": "^3.3.0",
"terser": "^5.15.1"
}
}

216
paper-io-bot.js Normal file
View File

@@ -0,0 +1,216 @@
if (process.argv.length < 3) {
console.log("Usage: node paper-io-bot.js <socket-url> [<name>]")
process.exit(1);
}
import io from "socket.io-client";
import * as client from "./src/game-client.js";
import { consts } from "./config.js";
const MOVES = [[-1, 0], [0, 1], [1, 0], [0, -1]];
let startFrame = -1;
let endFrame = -1;
let grid;
let others;
let user;
const playerPortion = {};
let claim = [];
function mod(x) {
x %= 4;
if (x < 0) x += 4;
return x;
}
function connect() {
const prefixes = consts.PREFIXES.split(" ");
const names = consts.NAMES.split(" ");
const name = process.argv[3] || [prefixes[Math.floor(Math.random() * prefixes.length)], names[Math.floor(Math.random() * names.length)]].join(" ");
client.connectGame(io, process.argv[2], "[BOT] " + name, function(success, msg) {
if (!success) {
console.error(msg);
setTimeout(connect, 1000);
}
});
}
function Loc(row, col) {
if (this.constructor != Loc) return new Loc(row, col);
this.row = row;
this.col = col;
}
function update(frame) {
if (startFrame == -1) startFrame = frame;
endFrame = frame;
if (frame % 6 == (startFrame + 1) % 6) {
grid = client.grid;
others = client.getOthers();
//Note: the code below isn't really my own code. This code is in fact the
//approximate algorithm used by the paper.io game. It has been modified from
//the original code (i.e. deobfuscating) and made more efficient in some
//areas (and some tweaks), otherwise, the original logic is about the same.
const row = user.row;
const col = user.col;
let dir = user.currentHeading;
const thres = (.05 + .1 * Math.random()) * consts.GRID_COUNT * consts.GRID_COUNT;
if (row < 0 || col < 0 || row >= consts.GRID_COUNT || col >= consts.GRID_COUNT) return;
if (grid.get(row, col) === user) {
//When we are inside our territory
claim = [];
const weights = [25, 25, 25, 25];
weights[dir] = 100;
weights[mod(dir + 2)] = -9999;
for (var nd = 0; nd < 4; nd++) {
for (var S = 1; S < 20; S++) {
var nr = MOVES[nd][0] * S + row;
var nc = MOVES[nd][1] * S + col;
if (nr < 0 || nc < 0 || nr >= consts.GRID_COUNT || nc >= consts.GRID_COUNT) {
if (S > 1) weights[nd]--;
else weights[nd] = -9999;
}
else {
if (grid.get(nr, nc) !== user) weights[nd]--;
var tailed = undefined;
for (var o of others) {
if (o.tail.hitsTail(new Loc(nr, nc))) {
tailed = o;
break;
}
}
if (tailed) {
if (o.name.indexOf("PAPER") != -1) weights[nd] += 3 * (30 - S); //Don't really try to kill our own kind
else weights[nd] += 30 * (30 - S);
}
}
}
}
//View a selection of choices based on the weights we computed
var choices = [];
for (var d = 0; d < 4; d++) {
for (var S = 1; S < weights[d]; S++) {
choices.push(d);
}
}
if (choices.length === 0) choices.push(dir);
dir = choices[Math.floor(Math.random() * choices.length)];
}
else if (playerPortion[user.num] < thres) {
//Claim some land if we are relatively tiny and have little to risk.
if (claim.length === 0) {
const breadth = 4 * Math.random() + 2;
const length = 4 * Math.random() + 2;
const ccw = 2 * Math.floor(2 * Math.random()) - 1;
const turns = [dir, mod(dir + ccw), mod(dir + ccw * 2), mod(dir + ccw * 3)];
const lengths = [breadth, length, breadth + 2 * Math.random() + 1, length];
for (let i = 0; i < turns.length; i++) {
for (let j = 0; j < lengths[i]; j++) {
claim.push(turns[i]);
}
}
}
if (claim.length !== 0) dir = claim.shift();
}
else {
claim = [];
//We are playing a little bit more cautious when we are outside and have a
//lot of land
const weights = [5, 5, 5, 5];
weights[dir] = 50;
weights[mod(dir + 2)] = -9999;
for (var nd = 0; nd < 4; nd++) {
for (var S = 1; S < 20; S++) {
var nr = MOVES[nd][0] * S + row;
var nc = MOVES[nd][1] * S + col;
if (nr < 0 || nc < 0 || nr >= consts.GRID_COUNT || nc >= consts.GRID_COUNT) {
if (S > 1) weights[nd]--;
else weights[nd] = -9999;
}
else {
if (user.tail.hitsTail(new Loc(nr, nc))) {
if (S > 1) weights[nd] -= 50 - S;
else weights[nd] = -9999;
}
if (grid.get(nr, nc) === user) weights[nd] += 10 + S;
var tailed = undefined;
for (var o of others) {
if (o.tail.hitsTail(new Loc(nr, nc))) {
tailed = o;
break;
}
}
if (tailed) {
if (o.name.indexOf("PAPER") != -1) weights[nd] += 3 * (30 - S); //Don't really try to kill our own kind
else weights[nd] += 30 * (30 - S);
}
}
}
}
//View a selection of choices based on the weights we computed
var choices = [];
for (var d = 0; d < 4; d++) {
for (var S = 1; S < weights[d]; S++) {
choices.push(d);
}
}
if (choices.length === 0) choices.push(dir);
dir = choices[Math.floor(Math.random() * choices.length)];
}
client.changeHeading(dir);
}
}
function calcFavorability(params) {
return params.portion + params.kills * 50 + params.survival / 100;
}
client.setAllowAnimation(false);
client.setRenderer({
addPlayer: function(player) {
playerPortion[player.num] = 0;
},
disconnect: function() {
const dt = (endFrame - startFrame);
startFrame = -1;
console.log(`[${new Date()}] I died... (survived for ${dt} frames.)`);
console.log(`[${new Date()}] I killed ${client.getKills()} player(s).`);
setTimeout(connect, 5000);
},
removePlayer: function(player) {
delete playerPortion[player.num];
},
setUser: function(u) {
user = u;
},
update,
updateGrid: function(row, col, before, after) {
before && playerPortion[before.num]--;
after && playerPortion[after.num]++;
}
});
connect();

165
public/css/styles.css Normal file
View File

@@ -0,0 +1,165 @@
/* arabic */
@font-face {
font-family: "Changa";
font-style: normal;
font-weight: 600;
src: local("Changa SemiBold"), local("Changa-SemiBold"), url(8-mw6umTgtMSI5PXqVIDMRJtnKITppOI_IvcXXDNrsc.woff2) format("woff2");
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+FB50-FDFF, U+FE80-FEFC;
}
/* latin-ext */
@font-face {
font-family: "Changa";
font-style: normal;
font-weight: 600;
src: local("Changa SemiBold"), local("Changa-SemiBold"), url(l9R1mHXzJ6oxjfw8BkQIThJtnKITppOI_IvcXXDNrsc.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Changa";
font-style: normal;
font-weight: 600;
src: local("Changa SemiBold"), local("Changa-SemiBold"), url(aXdbZDB08TCIBRKa7Qgu_FtXRa8TVwTICgirnJhmVJw.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
color: white;
font-family: "Changa", "Sans Serif";
user-select: none;
}
canvas {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
#begin, #wasted {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: rgba(0, 0, 0, .8);
}
#github svg {
transition: all 1s;
fill: #222;
color: #fff;
position: absolute;
top: 0;
left: 0;
border: 0;
width: 80px;
height: 80px;
transform: rotate(-90deg);
}
#github:hover svg {
width: 160px;
height: 160px;
}
.center {
text-align: center;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.center img {
display: block;
}
.scale {
animation: scale 5s infinite ease-in-out;
}
@keyframes scale {
0% {
transform: scale(0.8);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0.8);
}
}
input, button {
font-family: "Changa", "Sans Serif";
font-size: 1.5rem;
outline: none;
border: 0;
padding: .5rem 1rem;
line-height: 2.5rem;
}
button {
text-align: center;
cursor: pointer;
}
button:active {
transform: translateY(4px);
}
button:disabled {
cursor: not-allowed;
}
#name, #settings {
background: #ededd1;
border-bottom: 6px solid #a1a18d;
}
.yellow {
background: #eaec4b;
border-bottom: 6px solid #a1a130;
color: #888a34;
}
.yellow:hover {
background: #fafc5b;
}
.yellow:active {
border-bottom: 2px solid #a1a130;
}
.orange {
background: #FF972F;
border-bottom: 6px solid #AE4E0D;
color: #422100;
}
.orange:hover {
background: #FFB76F;
}
.orange:active {
border-bottom: 2px solid #AE4E0D;
}
.green {
background: #7fed4c;
border-bottom: 6px solid #56a130;
color: #4f8a34;
}
.green:hover {
background: #8efc5b;
}
.green:active {
border-bottom: 2px solid #56a130;
}
span {
color: red;
}
#wasted {
display: none;
}
#wasted img {
width: 650px;
}
.toggle {
position: fixed;
right: 5px;
bottom: 5px;
}
#settings {
display: grid;
padding: 20px;
}
#settings > * {
margin-top: 10px;
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/image/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

1
public/image/wasted.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

56
public/index.html Normal file
View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-touch-fullscreen" content="yes">
<title>Paper CATS</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="/font/css/all.min.css">
</head>
<body>
<canvas id="main-ui"></canvas>
<div id="begin">
<a id="github" href="https://git.sdf.org/ilikecats/papercats" target="_blank" title="Visit the code on SDF Git!">
<svg style="transform: rotate(90deg);" version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55"> <g> <path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/> <g> <g> <path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/> <path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"/> </g> </g> </g> </svg>
</a>
<div class="center">
<img class="scale" src="image/logo.png">
<h1>Enter your name</h1>
<input id="name" autocomplete="off" placeholder="An awesome name!">
<button class="start yellow" disabled="disabled">Play!</button>
<br>
<br>
<button id="mode" class="orange" disabled="disabled">Mode</button>
<button id="skins" class="yellow" disabled="disabled">Skins</button>
<button class="spectate green" disabled="disabled">Spectate</button>
<div>
<span id="error">Please wait... Maybe something's wrong!</span>
<noscript>
<span>Your browser does not support JavaScript!</span>
</noscript>
</div>
</div>
</div>
<div id="wasted">
<div class="center">
<img class="scale" src="image/wasted.svg">
<button class="start orange" disabled="disabled">Play Again</button>
<button class="menu yellow">Main Menu</button>
<button class="spectate green" disabled="disabled">Spectate</button>
</div>
</div>
<button class="toggle yellow"><i class="fa fa-cogs"></i></button>
<div id="settings" class="center" style="display: none;">
<button class="menu yellow">Main Menu</button>
<button class="yellow" onclick="JavaScript:alert('Want to play a game?\nUse arrows or WSAD to control your paper block\nDon\'t hit the walls\nBite enemy tails but don\'t let them bite yours!\nCompete against other players')">Hou To Play</button>
<button class="yellow" onclick="JavaScript:alert('Paper.io play online\nMore! More! More territory! Take it all with new amazing game - Paper.io\nThis game concept is linked to old Xonix, which appeared in 1984. Like in any other IO game there are you and enemies willing to outwit you. But you\'ll prevail of course :) Take their terriory and destroy your enemies, but be careful, your tail is your weak point.\nThis game is very cool and has nice paper-like graphics and fluid animation. Enjoy!\nYou can play paper.io online and offline both on a mobile device and a desktop computer. Get paper.io and join the world gaming community. Manage a small board and win territory from your rivals.\nPaper.io 2 - behold the sequel to the popular game. Capture new territories and become the king of the map!\nThe more space you win the higher ranking and scores you get. You have to act and think quickly. Develop your own strategy and action plan.\nPaperio has simple rules but is very addictive in its simplicity. The competitors are also on guard. Watch out for your tail.\nThe game is very addictive and makes gamers from all over the world connect from home, from work, from campus or even the office!\nJoin the game, figure out your strategy and become the ultimate winner. Have fun!')">About</button>
<audio loop="loop" preload="auto" controls>
<source src="https://ilikecats.sdf.org/UNTALE/43%20Temmie%20Village.flac" type="audio/flac">
</audio>
</div>
<script src="js/bundle.js"></script>
</body>
</html>

0
public/js/.gitkeep Normal file
View File

BIN
public/music/gwent.mp3 Executable file

Binary file not shown.

BIN
public/music/gwent.ogg Executable file

Binary file not shown.

7
rollup.config.js Normal file
View File

@@ -0,0 +1,7 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'client.js',
plugins: [resolve(), commonjs()]
};

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

54
server.js Normal file
View File

@@ -0,0 +1,54 @@
// https://github.com/socketio/socket.io/blob/master/examples/chat/index.js
import MiServer from "mimi-server";
import { Server } from "socket.io";
import express from "express";
import path from "path";
import { exec, fork } from "child_process";
import { config } from "./config.js";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
config.dev ? exec("npm run build-dev") : exec("npm run build");
const port = process.env.PORT || config.port;
const { app, server } = new MiServer({
port,
static: path.join(__dirname, "public")
});
const io = new Server(server);
// Routing
app.use("/font", express.static(path.join(__dirname, "node_modules/@fortawesome/fontawesome-free")));
import Game from "./src/game-server.js";
const game = new Game();
io.on("connection", socket => {
socket.on("hello", (data, fn) => {
//TODO: error checking.
if (data.god && game.addGod(socket)) {
fn(true);
return;
}
if (data.name && data.name.length > 32) fn(false, "Your name is too long!");
else if (!game.addPlayer(socket, data.name)) fn(false, "There're too many platers!");
else fn(true);
});
socket.on("pings", (fn) => {
socket.emit("pongs");
socket.disconnect();
});
});
setInterval(() => {
game.tickFrame();
}, 1000 / 60);
for (let i = 0; i < parseInt(config.bots); i++) {
fork(path.join(__dirname, "paper-io-bot.js"), [`ws://localhost:${port}`], {
stdio: "inherit"
});
}

120
src/core/color.js Normal file
View File

@@ -0,0 +1,120 @@
function verifyRange() {
for (let i = 0; i < arguments.length; i++) {
if (arguments[i] < 0 || arguments[i] > 1) throw new RangeError("H, S, L, and A parameters must be between the range [0, 1]");
}
}
// https://stackoverflow.com/a/9493060/7344257
function hslToRgb(h, s, l) {
let r, g, b;
if (s == 0) r = g = b = l; //Achromatic
else {
const hue2rgb = function(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
class Color {
constructor(h, s, l, a) {
verifyRange(h, s, l);
if (a === undefined) a = 1;
else verifyRange(a);
Object.defineProperties(this, {
"hue": {
value: h,
enumerable: true
},
"sat": {
value: s,
enumerable: true
},
"lum": {
value: l,
enumerable: true
},
"alpha": {
value: a,
enumerable: true
},
});
}
interpolateToString(color, amount) {
const rgbThis = hslToRgb(this.hue, this.sat, this.lum);
const rgbThat = hslToRgb(color.hue, color.sat, color.lum);
const rgb = [];
for (let i = 0; i < 3; i++) {
rgb[i] = Math.floor((rgbThat[i] - rgbThis[i]) * amount + rgbThis[i]);
}
return {
rgbString: function() {
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`
}
};
}
deriveLumination(amount) {
let lum = this.lum + amount;
lum = Math.min(Math.max(lum, 0), 1);
return new Color(this.hue, this.sat, lum, this.alpha);
}
deriveHue(amount) {
const hue = this.hue - amount;
return new Color(hue - Math.floor(hue), this.sat, this.lum, this.alpha);
}
deriveSaturation(amount) {
let sat = this.sat + amount;
sat = Math.min(Math.max(sat, 0), 1);
return new Color(this.hue, sat, this.lum, this.alpha);
}
deriveAlpha(newAlpha) {
verifyRange(newAlpha);
return new Color(this.hue, this.sat, this.lum, newAlpha);
}
rgbString() {
const rgb = hslToRgb(this.hue, this.sat, this.lum);
rgb[3] = this.a;
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${this.alpha})`;
}
}
Color.fromData = data => {
return new Color(data.hue, data.sat, data.lum, data.alpha);
};
Color.possColors = () => {
const SATS = [192, 150, 100].map(val => val / 240);
const HUES = [0, 10, 20, 25, 30, 35, 40, 45, 50, 60, 70, 100, 110, 120, 125, 130, 135, 140, 145, 150, 160, 170, 180, 190, 200, 210, 220].map(val => val / 240);
const possColors = new Array(SATS.length * HUES.length);
let i = 0;
for (let s = 0; s < SATS.length; s++) {
for (let h = 0; h < HUES.length; h++) {
possColors[i++] = new Color(HUES[h], SATS[s], .5, 1);
}
}
//Shuffle the colors
for (let i = 0; i < possColors.length * 50; i++) {
const a = Math.floor(Math.random() * possColors.length);
const b = Math.floor(Math.random() * possColors.length);
const tmp = possColors[a];
possColors[a] = possColors[b];
possColors[b] = tmp;
}
return possColors;
}
export default Color;

42
src/core/grid.js Normal file
View File

@@ -0,0 +1,42 @@
function Grid(size, changeCallback) {
let grid = new Array(size);
let modified = false;
const data = {
grid,
size
};
this.get = (row, col) => {
if (isOutOfBounds(data, row, col)) throw new RangeError("Row or Column value out of bounds");
return grid[row] && grid[row][col];
}
this.set = (row, col, value) => {
if (isOutOfBounds(data, row, col)) throw new RangeError("Row or Column value out of bounds");
if (!grid[row]) grid[row] = new Array(size);
const before = grid[row][col];
grid[row][col] = value;
if (typeof changeCallback === "function") changeCallback(row, col, before, value);
modified = true;
return before;
}
this.reset = () => {
if (modified) {
grid = new Array(size);
modified = false;
}
}
this.isOutOfBounds = isOutOfBounds.bind(this, data);
Object.defineProperty(this, "size", {
get: function() {
return size;
},
enumerable: true
});
}
function isOutOfBounds(data, row, col) {
return row < 0 || row >= data.size || col < 0 || col >= data.size;
}
export default Grid;

109
src/core/index.js Normal file
View File

@@ -0,0 +1,109 @@
import { consts } from "../../config.js";
export { default as Color } from "./color.js";
export { default as Grid } from "./grid.js";
export { default as Player } from "./player.js";
export function initPlayer(grid, player) {
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (!grid.isOutOfBounds(dr + player.row, dc + player.col)) grid.set(dr + player.row, dc + player.col, player);
}
}
}
export function updateFrame(grid, players, dead, notifyKill) {
let adead = [];
if (dead instanceof Array) adead = dead;
//Move players
let tmp = players.filter(val => {
val.move();
if (val.dead) adead.push(val);
return !val.dead;
});
//Remove players with collisions
const removing = new Array(players.length);
const kill = (!notifyKill) ? () => { } : (killer, other) => {
if (!removing[other]) notifyKill(killer, other);
};
for (let i = 0; i < players.length; i++) {
for (let j = i; j < players.length; j++) {
//Remove those players when other players have hit their tail
if (!removing[j] && players[j].tail.hitsTail(players[i])) {
kill(i, j);
removing[j] = true;
//console.log("TAIL");
}
if (!removing[i] && players[i].tail.hitsTail(players[j])) {
kill(j, i);
removing[i] = true;
//console.log("TAIL");
}
//Remove players with collisons...
if (i !== j && squaresIntersect(players[i].posX, players[j].posX) &&
squaresIntersect(players[i].posY, players[j].posY)) {
//...if one player is own his own territory, the other is out
if (grid.get(players[i].row, players[i].col) === players[i]) {
kill(i, j);
removing[j] = true;
}
else if (grid.get(players[j].row, players[j].col) === players[j]) {
kill(j, i);
removing[i] = true;
}
else {
//...otherwise, the one that sustains most of the collision will be removed
const areaI = area(players[i]);
const areaJ = area(players[j]);
if (areaI === areaJ) {
kill(i, j);
kill(j, i);
removing[i] = removing[j] = true;
}
else if (areaI > areaJ) {
kill(j, i);
removing[i] = true;
}
else {
kill(i, j);
removing[j] = true;
}
}
}
}
}
tmp = tmp.filter((val, i) => {
if (removing[i]) {
adead.push(val);
val.die();
}
return !removing[i];
});
players.length = tmp.length;
for (let i = 0; i < tmp.length; i++) {
players[i] = tmp[i];
}
//Remove dead squares
for (let r = 0; r < grid.size; r++) {
for (let c = 0; c < grid.size; c++) {
if (adead.indexOf(grid.get(r, c)) !== -1) grid.set(r, c, null);
}
}
}
function squaresIntersect(a, b) {
return (a < b) ? (b < a + consts.CELL_WIDTH) : (a < b + consts.CELL_WIDTH);
}
function area(player) {
const xDest = player.col * consts.CELL_WIDTH;
const yDest = player.row * consts.CELL_WIDTH;
return (player.posX === xDest) ? Math.abs(player.posY - yDest) : Math.abs(player.posX - xDest);
}

424
src/core/player.js Normal file
View File

@@ -0,0 +1,424 @@
import Stack from "./stack.js";
import Color from "./color.js";
import Grid from "./grid.js";
import { consts } from "../../config.js";
function defineGetter(getter) {
return {
get: getter,
enumerable: true
};
}
function defineInstanceMethods(thisobj, data /*, methods...*/) {
for (let i = 2; i < arguments.length; i++) {
thisobj[arguments[i].name] = arguments[i].bind(this, data);
}
}
function defineAccessorProperties(thisobj, data /*, names...*/) {
const descript = {};
function getAt(name) {
return () => {
return data[name];
};
}
for (let i = 2; i < arguments.length; i++) {
descript[arguments[i]] = defineGetter(getAt(arguments[i]));
}
Object.defineProperties(thisobj, descript);
}
function TailMove(orientation) {
this.move = 1;
Object.defineProperty(this, "orientation", {
value: orientation,
enumerable: true
});
}
function Tail(player, sdata) {
const data = {
tail: [],
tailGrid: [],
prev: null,
startRow: 0,
startCol: 0,
prevRow: 0,
prevCol: 0,
player
};
if (sdata) {
data.startRow = data.prevRow = sdata.startRow || 0;
data.startCol = data.prevCol = sdata.startCol || 0;
sdata.tail.forEach(val => {
addTail(data, val.orientation, val.move);
});
}
data.grid = player.grid;
defineInstanceMethods(this, data, addTail, hitsTail, fillTail, renderTail, reposition, serialData);
Object.defineProperty(this, "moves", {
get: function() {
return data.tail.slice(0);
},
enumerable: true
});
}
//Instance methods
function serialData(data) {
return {
tail: data.tail,
startRow: data.startRow,
startCol: data.startCol
};
}
function setTailGrid(data, tailGrid, r, c) {
if (!tailGrid[r]) tailGrid[r] = [];
tailGrid[r][c] = true;
}
function addTail(data, orientation, count) {
if (count === undefined) count = 1;
if (!count || count < 0) return;
let prev = data.prev;
const r = data.prevRow, c = data.prevCol;
if (data.tail.length === 0) setTailGrid(data, data.tailGrid, r, c);
if (!prev || prev.orientation !== orientation) {
prev = data.prev = new TailMove(orientation);
data.tail.push(prev);
prev.move += count - 1;
}
else prev.move += count;
for (let i = 0; i < count; i++) {
const pos = walk([data.prevRow, data.prevCol], null, orientation, 1);
data.prevRow = pos[0];
data.prevCol = pos[1];
setTailGrid(data, data.tailGrid, pos[0], pos[1]);
}
}
function reposition(data, row, col) {
data.prevRow = data.startRow = row;
data.prevCol = data.startCol = col;
data.prev = null;
if (data.tail.length === 0) return;
else {
const ret = data.tail;
data.tail = [];
data.tailGrid = [];
return ret;
}
}
//Helper methods
function renderTail(data, ctx) {
if (data.tail.length === 0) return;
ctx.fillStyle = data.player.tailColor.rgbString();
let prevOrient = -1;
let start = [data.startRow, data.startCol];
//fillTailRect(ctx, start, start);
data.tail.forEach(tail => {
const negDir = tail.orientation === 0 || tail.orientation === 3;
const back = start;
if (!negDir) start = walk(start, null, tail.orientation, 1);
const finish = walk(start, null, tail.orientation, tail.move - 1);
if (tail.move > 1) fillTailRect(ctx, start, finish);
if (prevOrient !== -1) renderCorner(ctx, back, prevOrient, tail.orientation);
//Draw folding triangle.
start = finish;
if (negDir) walk(start, start, tail.orientation, 1);
prevOrient = tail.orientation;
});
const curOrient = data.player.currentHeading;
if (prevOrient === curOrient) fillTailRect(ctx, start, start);
else renderCorner(ctx, start, prevOrient, curOrient);
}
function renderCorner(ctx, cornerStart, dir1, dir2) {
if (dir1 === 0 || dir2 === 0) walk(cornerStart, cornerStart, 2, 1);
if (dir1 === 3 || dir2 === 3) walk(cornerStart, cornerStart, 1, 1);
const a = walk(cornerStart, null, dir2, 1);
const b = walk(a, null, dir1, 1);
const triangle = new Path2D();
triangle.moveTo(cornerStart[1] * consts.CELL_WIDTH, cornerStart[0] * consts.CELL_WIDTH);
triangle.lineTo(a[1] * consts.CELL_WIDTH, a[0] * consts.CELL_WIDTH);
triangle.lineTo(b[1] * consts.CELL_WIDTH, b[0] * consts.CELL_WIDTH);
triangle.closePath();
for (let i = 0; i < 2; i++) {
ctx.fill(triangle);
}
}
function walk(from, ret, orient, dist) {
ret = ret || [];
ret[0] = from[0];
ret[1] = from[1];
switch (orient) {
case 0: ret[0] -= dist; break; //UP
case 1: ret[1] += dist; break; //RIGHT
case 2: ret[0] += dist; break; //DOWN
case 3: ret[1] -= dist; break; //LEFT
}
return ret;
}
function fillTailRect(ctx, start, end) {
let x = start[1] * consts.CELL_WIDTH;
let y = start[0] * consts.CELL_WIDTH;
let width = (end[1] - start[1]) * consts.CELL_WIDTH;
let height = (end[0] - start[0]) * consts.CELL_WIDTH;
if (width === 0) width += consts.CELL_WIDTH;
if (height === 0) height += consts.CELL_WIDTH;
if (width < 0) {
x += width;
width = -width;
}
if (height < 0) {
y += height;
height = -height;
}
ctx.fillRect(x, y, width, height);
}
function fillTail(data) {
if (data.tail.length === 0) return;
function onTail(c) {
return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]];
}
const grid = data.grid;
const start = [data.startRow, data.startCol];
const been = new Grid(grid.size);
const coords = [];
coords.push(start);
while (coords.length > 0) { //BFS for all tail spaces
const coord = coords.shift();
const r = coord[0];
const c = coord[1];
if (grid.isOutOfBounds(r, c) || been.get(r, c)) continue;
if (onTail(coord)) {//On the tail
been.set(r, c, true);
grid.set(r, c, data.player);
//Find all spots that this tail encloses
floodFill(data, grid, r + 1, c, been);
floodFill(data, grid, r - 1, c, been);
floodFill(data, grid, r, c + 1, been);
floodFill(data, grid, r, c - 1, been);
coords.push([r + 1, c]);
coords.push([r - 1, c]);
coords.push([r, c + 1]);
coords.push([r, c - 1]);
}
}
}
function floodFill(data, grid, row, col, been) {
function onTail(c) {
return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]];
}
const start = [row, col];
if (grid.isOutOfBounds(row, col) || been.get(row, col) || onTail(start) || grid.get(row, col) === data.player) return;
//Avoid allocating too many resources
const coords = [];
const filled = new Stack(consts.GRID_COUNT * consts.GRID_COUNT + 1);
let surrounded = true;
let coord;
coords.push(start);
while (coords.length > 0) {
coord = coords.shift();
const r = coord[0];
const c = coord[1];
if (grid.isOutOfBounds(r, c)) {
surrounded = false;
continue;
}
//End this traverse on boundaries (where we been, on the tail, and when we enter our territory)
if (been.get(r, c) || onTail(coord) || grid.get(r, c) === data.player) continue;
been.set(r, c, true);
if (surrounded) filled.push(coord);
coords.push([r + 1, c]);
coords.push([r - 1, c]);
coords.push([r, c + 1]);
coords.push([r, c - 1]);
}
if (surrounded) {
while (!filled.isEmpty()) {
coord = filled.pop();
grid.set(coord[0], coord[1], data.player);
}
}
return surrounded;
}
function hitsTail(data, other) {
return (data.prevRow !== other.row || data.prevCol !== other.col)
&& (data.startRow !== other.row || data.startCol !== other.col)
&& !!(data.tailGrid[other.row] && data.tailGrid[other.row][other.col]);
}
const SHADOW_OFFSET = 10;
function Player(grid, sdata) {
const data = {};
//Parameters
data.num = sdata.num;
data.name = sdata.name || "Player " + (data.num + 1);
data.grid = grid;
data.posX = sdata.posX;
data.posY = sdata.posY;
this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left
data.waitLag = sdata.waitLag || 0;
data.dead = false;
//Only need colors for client side
let base;
if (sdata.base) base = this.baseColor = sdata.base instanceof Color ? sdata.base : Color.fromData(sdata.base);
else {
const hue = Math.random();
this.baseColor = base = new Color(hue, .8, .5);
}
this.lightBaseColor = base.deriveLumination(.1);
this.shadowColor = base.deriveLumination(-.3);
this.tailColor = base.deriveLumination(.2).deriveAlpha(0.98);
//Tail requires special handling
this.grid = grid; //Temporary
if (sdata.tail) data.tail = new Tail(this, sdata.tail);
else {
data.tail = new Tail(this);
data.tail.reposition(calcRow(data), calcCol(data));
}
//Instance methods
this.move = move.bind(this, data);
this.die = () => { data.dead = true; };
this.serialData = function() {
return {
base: this.baseColor,
num: data.num,
name: data.name,
posX: data.posX,
posY: data.posY,
currentHeading: data.currentHeading,
tail: data.tail.serialData(),
waitLag: data.waitLag
};
};
//Read-only Properties
defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail", "waitLag");
Object.defineProperties(this, {
row: defineGetter(() => calcRow(data)),
col: defineGetter(() => calcCol(data))
});
}
//Gets the next integer in positive or negative direction
function nearestInteger(positive, val) {
return positive ? Math.ceil(val) : Math.floor(val);
}
function calcRow(data) {
return nearestInteger(data.currentHeading === 2 /*DOWN*/, data.posY / consts.CELL_WIDTH);
}
function calcCol(data) {
return nearestInteger(data.currentHeading === 1 /*RIGHT*/, data.posX / consts.CELL_WIDTH);
}
//Instance methods
Player.prototype.render = function(ctx, fade) {
//Render tail
this.tail.renderTail(ctx);
//Render player
fade = fade || 1;
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
ctx.fillRect(this.posX, this.posY, consts.CELL_WIDTH, consts.CELL_WIDTH);
const mid = consts.CELL_WIDTH / 2;
const grd = ctx.createRadialGradient(this.posX + mid, this.posY + mid - SHADOW_OFFSET, 1, this.posX + mid, this.posY + mid - SHADOW_OFFSET, consts.CELL_WIDTH);
//grd.addColorStop(0, this.baseColor.deriveAlpha(fade).rgbString());
//grd.addColorStop(1, new Color(0, 0, 1, fade).rgbString());
//ctx.fillStyle = grd;
ctx.fillStyle = this.shadowColor.deriveLumination(.2).rgbString();
ctx.fillRect(this.posX - 1, this.posY - SHADOW_OFFSET, consts.CELL_WIDTH + 2, consts.CELL_WIDTH);
//Render name
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
ctx.textAlign = "center";
let yoff = -SHADOW_OFFSET * 2;
if (this.row === 0) yoff = SHADOW_OFFSET * 2 + consts.CELL_WIDTH;
ctx.font = "18px Changa";
ctx.fillText(this.name, this.posX + consts.CELL_WIDTH / 2, this.posY + yoff);
};
function move(data) {
if (data.waitLag < consts.NEW_PLAYER_LAG) { //Wait for a second at least
data.waitLag++;
return;
}
//Move to new position
let { heading } = this;
if (this.posX % consts.CELL_WIDTH !== 0 || this.posY % consts.CELL_WIDTH !== 0) heading = data.currentHeading;
else data.currentHeading = heading;
switch (heading) {
case 0: data.posY -= consts.SPEED; break; //UP
case 1: data.posX += consts.SPEED; break; //RIGHT
case 2: data.posY += consts.SPEED; break; //DOWN
case 3: data.posX -= consts.SPEED; break; //LEFT
}
//Check for out of bounds
const { row, col } = this;
if (data.grid.isOutOfBounds(row, col)) {
switch (heading) {
case 0: data.posY += consts.SPEED; break;
case 1: data.posX -= consts.SPEED; break;
case 2: data.posY -= consts.SPEED; break;
case 3: data.posX += consts.SPEED; break;
}
//data.dead = true;
return;
}
//Update tail position
if (data.grid.get(row, col) === this) {
//Safe zone!
this.tail.fillTail();
this.tail.reposition(row, col);
}
//If we are completely in a new cell (not in our safe zone), we add to the tail
else if (this.posX % consts.CELL_WIDTH === 0 && this.posY % consts.CELL_WIDTH === 0) this.tail.addTail(heading);
}
export default Player;

28
src/core/stack.js Normal file
View File

@@ -0,0 +1,28 @@
function Stack(initSize) {
let len = 0;
const arr = [];
this.ensureCapacity = size => {
arr.length = Math.max(arr.length, size || 0);
};
this.push = function(ele) {
this[len] = ele;
len++;
};
this.pop = function() {
if (len === 0) return;
len--;
const tmp = this[len];
this[len] = undefined;
return tmp;
};
this.isEmpty = () => {
return len === 0;
}
this.ensureCapacity(initSize);
Object.defineProperty(this, "length", {
get: function() {
return len;
}
});
}
export default Stack;

279
src/game-client.js Normal file
View File

@@ -0,0 +1,279 @@
import { Grid, Player, initPlayer, updateFrame } from "./core/index.js";
import { consts } from "../config.js";
let running = false;
let user, socket, frame;
let players, allPlayers;
let kills;
let timeout = undefined;
let dirty = false;
let deadFrames = 0;
let requesting = -1; //Frame that we are requesting at
let frameCache = []; //Frames after our request
let _allowAnimation = true;
const grid = new Grid(consts.GRID_COUNT, (row, col, before, after) => {
invokeRenderer("updateGrid", [row, col, before, after]);
});
let renderer;
let requestAnimationFrame;
try {
requestAnimationFrame = window.requestAnimationFrame;
} catch {
requestAnimationFrame = callback => { setTimeout(callback, 1000 / 30) };
}
//Public API
function connectGame(io, url, name, callback, flag) {
if (running) return; //Prevent multiple runs
running = true;
user = null;
deadFrames = 0;
const prefixes = consts.PREFIXES.split(" ");
const names = consts.NAMES.split(" ");
name = name || [prefixes[Math.floor(Math.random() * prefixes.length)], names[Math.floor(Math.random() * names.length)]].join(" ");
//Socket connection
io.j = [];
io.sockets = [];
socket = io(url, {
"forceNew": true,
upgrade: false,
transports: ["websocket"]
});
socket.on("connect", () => {
console.info("Connected to server.");
});
socket.on("game", data => {
if (timeout != undefined) clearTimeout(timeout);
//Initialize game
//TODO: display data.gameid --- game id #
frame = data.frame;
reset();
//Load players
data.players.forEach(p => {
const pl = new Player(grid, p);
addPlayer(pl);
});
user = allPlayers[data.num];
//if (!user) throw new Error();
setUser(user);
//Load grid
const gridData = new Uint8Array(data.grid);
for (let r = 0; r < grid.size; r++) {
for (let c = 0; c < grid.size; c++) {
const ind = gridData[r * grid.size + c] - 1;
grid.set(r, c, ind === -1 ? null : players[ind]);
}
}
invokeRenderer("paint", []);
frame = data.frame;
if (requesting !== -1) {
//Update those cache frames after we updated game
const minFrame = requesting;
requesting = -1;
while (frameCache.length > frame - minFrame) processFrame(frameCache[frame - minFrame]);
frameCache = [];
}
});
socket.on("notifyFrame", processFrame);
socket.on("dead", () => {
socket.disconnect(); //In case we didn"t get the disconnect call
});
socket.on("disconnect", () => {
console.info("Server has disconnected. Creating new game.");
socket.disconnect();
if (!user) return;
user.die();
dirty = true;
paintLoop();
running = false;
invokeRenderer("disconnect", []);
});
socket.emit("hello", {
name: name,
type: 0, //Free-for-all
gameid: -1, //Requested game-id, or -1 for anyone
god: flag
}, (success, msg) => {
if (success) console.info("Connected to game!");
else {
console.error("Unable to connect to game: " + msg);
running = false;
}
if (callback) callback(success, msg);
});
}
function changeHeading(newHeading) {
if (!user || user.dead) return;
if (newHeading === user.currentHeading || ((newHeading % 2 === 0) ^ (user.currentHeading % 2 === 0))) {
//user.heading = newHeading;
if (socket) {
socket.emit("frame", {
frame: frame,
heading: newHeading
}, (success, msg) => {
if (!success) console.error(msg);
});
}
}
}
function getUser() {
return user;
}
function getPlayers() {
return players.slice();
}
function getOthers() {
const ret = [];
for (const p of players) {
if (p !== user) ret.push(p);
}
return ret;
}
function disconnect() {
socket.disconnect();
running = false;
}
//Private API
function addPlayer(player) {
if (allPlayers[player.num]) return; //Already added
allPlayers[player.num] = players[players.length] = player;
invokeRenderer("addPlayer", [player]);
return players.length - 1;
}
function invokeRenderer(name, args) {
if (renderer && typeof renderer[name] === "function") renderer[name].apply(null, args);
}
function processFrame(data) {
if (timeout != undefined) clearTimeout(timeout);
if (requesting !== -1 && requesting < data.frame) {
frameCache.push(data);
return;
}
if (data.frame - 1 !== frame) {
console.error("Frames don't match up!");
socket.emit("requestFrame"); //Restore data
requesting = data.frame;
frameCache.push(data);
return;
}
frame++;
if (data.newPlayers) {
data.newPlayers.forEach(p => {
if (user && p.num === user.num) return;
const pl = new Player(grid, p);
addPlayer(pl);
initPlayer(grid, pl);
});
}
const found = new Array(players.length);
data.moves.forEach((val, i) => {
const player = allPlayers[val.num];
if (!player) return;
if (val.left) player.die();
found[i] = true;
player.heading = val.heading;
});
for (let i = 0; i < players.length; i++) {
//Implicitly leaving game
if (!found[i]) {
const player = players[i];
player && player.die();
}
}
update();
const locs = {};
for (let i = 0; i < players.length; i++) {
const p = players[i];
locs[p.num] = [p.posX, p.posY, p.waitLag];
}
dirty = true;
requestAnimationFrame(paintLoop);
timeout = setTimeout(() => {
console.warn("Server has timed-out. Disconnecting.");
socket.disconnect();
}, 3000);
}
function paintLoop() {
if (!dirty) return;
invokeRenderer("paint", []);
dirty = false;
if (user && user.dead) {
if (timeout) clearTimeout(timeout);
if (deadFrames === 60) { //One second of frame
const before = _allowAnimation;
_allowAnimation = false;
update();
invokeRenderer("paint", []);
_allowAnimation = before;
user = null;
deadFrames = 0;
return;
}
socket.disconnect();
deadFrames++;
dirty = true;
update();
requestAnimationFrame(paintLoop);
}
}
function reset() {
user = null;
grid.reset();
players = [];
allPlayers = [];
kills = 0;
invokeRenderer("reset");
}
function setUser(player) {
user = player;
invokeRenderer("setUser", [player]);
}
function update() {
const dead = [];
updateFrame(grid, players, dead, (killer, other) => { //addKill
if (players[killer] === user && killer !== other) kills++;
});
dead.forEach(val => {
console.log((val.name || "Unnamed") + " is dead");
delete allPlayers[val.num];
invokeRenderer("removePlayer", [val]);
});
invokeRenderer("update", [frame]);
}
function setRenderer(r) {
renderer = r;
}
function setAllowAnimation(allow) {
_allowAnimation = allow;
}
function getKills() {
return kills;
}
// Export stuff
export { connectGame, changeHeading, getUser, getPlayers, getOthers, disconnect, setRenderer, setAllowAnimation, getKills };
export const allowAnimation = {
get: function() {
return _allowAnimation;
},
set: function(val) {
_allowAnimation = !!val;
},
enumerable: true
};
export { grid };

239
src/game-server.js Normal file
View File

@@ -0,0 +1,239 @@
import { Color, Grid, Player, initPlayer, updateFrame } from "./core/index.js";
import { consts } from "../config.js";
function Game(id) {
const possColors = Color.possColors();
let nextInd = 0;
const players = [];
const gods = [];
let newPlayers = [];
const frameLocs = [];
let frame = 0;
let filled = 0;
const grid = new Grid(consts.GRID_COUNT, (row, col, before, after) => {
if (!!after ^ !!before) {
if (after) filled++;
else filled--;
if (filled === consts.GRID_COUNT * consts.GRID_COUNT) console.log(`[${new Date()}] FULL GAME`);
}
});
this.id = id;
this.addPlayer = (client, name) => {
if (players.length >= consts.MAX_PLAYERS) return false;
const start = findEmpty(grid);
if (!start) return false;
const params = {
posX: start.col * consts.CELL_WIDTH,
posY: start.row * consts.CELL_WIDTH,
currentHeading: Math.floor(Math.random() * 4),
name,
num: nextInd,
base: possColors.shift()
};
const p = new Player(grid, params);
p.tmpHeading = params.currentHeading;
p.client = client;
players.push(p);
newPlayers.push(p);
nextInd++;
initPlayer(grid, p);
if (p.name.indexOf("[BOT]") == -1) console.log(`[${new Date()}] ${p.name || "Unnamed"} (${p.num}) joined.`);
client.on("requestFrame", () => {
if (p.frame === frame) return;
p.frame = frame; //Limit number of requests per frame (One per frame)
const splayers = players.map(val => val.serialData());
client.emit("game", {
"num": p.num,
"gameid": id,
"frame": frame,
"players": splayers,
"grid": gridSerialData(grid, players)
});
});
client.on("frame", (data, errorHan) => {
if (typeof data === "function") {
errorHan(false, "No data supplied.");
return;
}
if (typeof errorHan !== "function") errorHan = () => {};
if (!data) errorHan(false, "No data supplied.");
else if (!checkInt(data.frame, 0, Infinity)) errorHan(false, "Requires a valid non-negative frame integer.");
else if (data.frame > frame) errorHan(false, "Invalid frame received.");
else {
if (data.heading !== undefined) {
if (checkInt(data.heading, 0, 4)) {
p.tmpHeading = data.heading;
errorHan(true);
}
else errorHan(false, "New heading must be an integer of range [0, 4).");
}
}
});
client.on("disconnect", () => {
p.die(); //Die immediately if not already
p.disconnected = true;
if (p.name.indexOf("[BOT]") == -1) console.log(`[${new Date()}] ${p.name || "Unnamed"} (${p.num}) left.`);
});
return true;
};
this.addGod = client => {
const g = {
client,
frame
};
gods.push(g);
const splayers = players.map(val => val.serialData());
client.emit("game", {
"gameid": id,
"frame": frame,
"players": splayers,
"grid": gridSerialData(grid, players)
});
client.on("requestFrame", () => {
if (g.frame === frame) return;
g.frame = frame; //Limit number of requests per frame (One per frame)
const splayers = players.map(val => val.serialData());
g.client.emit("game", {
"gameid": id,
"frame": frame,
"players": splayers,
"grid": gridSerialData(grid, players)
});
});
return true;
};
function pushPlayerLocations() {
const locs = [];
for (const p of players) {
locs[p.num] = [p.posX, p.posY, p.waitLag];
}
locs.frame = frame;
if (frameLocs.length >= 300) frameLocs.shift(); //Give it 5 seconds of lag
frameLocs.push(locs);
}
function verifyPlayerLocations(fr, verify, resp) {
const minFrame = frame - frameLocs.length + 1;
if (fr < minFrame || fr > frame) {
resp(false, false, "Frames out of reference");
return;
}
function string(loc) {
return `(${loc[0]}, ${loc[1]}) [${loc[2]}]`;
}
const locs = frameLocs[fr - minFrame];
if (locs.frame !== fr) {
resp(false, false, locs.frame + " != " + fr);
return;
}
for (const num in verify) {
if (!locs[num]) continue;
if (locs[num][0] !== verify[num][0] || locs[num][1] !== verify[num][1] || locs[num][2] !== verify[num][2]) {
resp(false, true, "P" + num + " " + string(locs[num]) + " !== " + string(verify[num]));
return;
}
}
resp(true, false);
}
function tick() {
//TODO: notify those players that this server automatically drops out
const splayers = players.map(val => val.serialData());
const snews = newPlayers.map(val => {
//Emit game stats.
val.client.emit("game", {
"num": val.num,
"gameid": id,
"frame": frame,
"players": splayers,
"grid": gridSerialData(grid, players),
});
return val.serialData();
});
const moves = players.map(val => {
//Account for race condition (when heading is set after emitting frames, and before updating)
val.heading = val.tmpHeading;
return {
num: val.num,
left: !!val.disconnected,
heading: val.heading
};
});
update();
const data = {
frame: frame + 1,
moves
};
if (snews.length > 0) {
data.newPlayers = snews;
newPlayers = [];
}
for (const p of players) {
p.client.emit("notifyFrame", data);
}
for (const g of gods) {
g.client.emit("notifyFrame", data);
}
frame++;
pushPlayerLocations();
}
this.tickFrame = tick;
function update() {
const dead = [];
updateFrame(grid, players, dead);
for (const p of dead) {
if (!p.handledDead) {
possColors.push(p.baseColor);
p.handledDead = true;
}
if (p.name.indexOf("[BOT]") == -1) console.log(`${p.name || "Unnamed"} (${p.num}) died.`);
p.client.emit("dead");
p.client.disconnect(true);
}
}
}
function checkInt(value, min, max) {
return !(typeof value !== "number" || value < min || value >= max || Math.floor(value) !== value);
}
function gridSerialData(grid, players) {
const buff = Buffer.alloc(grid.size * grid.size);
const numToIndex = new Array(players.length > 0 ? players[players.length - 1].num + 1 : 0);
for (let i = 0; i < players.length; i++) {
numToIndex[players[i].num] = i + 1;
}
for (let r = 0; r < grid.size; r++) {
for (let c = 0; c < grid.size; c++) {
const ele = grid.get(r, c);
buff[r * grid.size + c] = ele ? numToIndex[ele.num] : 0;
}
}
return buff;
}
function findEmpty(grid) {
const available = [];
for (let r = 1; r < grid.size - 1; r++) {
for (let c = 1; c < grid.size - 1; c++) {
let cluttered = false;
checkclutter: for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (grid.get(r + dr, c + dc)) {
cluttered = true;
break checkclutter;
}
}
}
if (!cluttered) available.push({
row: r,
col: c
});
}
}
return (available.length === 0) ? null : available[Math.floor(available.length * Math.random())];
}
export default Game;

334
src/mode/god.js Normal file
View File

@@ -0,0 +1,334 @@
import jquery from "jquery";
import { Grid, Color } from "../core";
import * as client from "../game-client";
import { consts } from "../../config.js";
const SHADOW_OFFSET = 5;
const ANIMATE_FRAMES = 24;
const BOUNCE_FRAMES = [8, 4];
const DROP_HEIGHT = 24;
const DROP_SPEED = 2;
const MIN_BAR_WIDTH = 65;
const BAR_HEIGHT = SHADOW_OFFSET + consts.CELL_WIDTH;
const BAR_WIDTH = 400;
let canvas, ctx, offscreenCanvas, offctx, canvasWidth, canvasHeight, gameWidth, gameHeight;
const $ = jquery;
$(() => {
canvas = $("#main-ui")[0];
ctx = canvas.getContext("2d");
offscreenCanvas = document.createElement("canvas");
offctx = offscreenCanvas.getContext("2d");
updateSize();
});
let animateGrid, playerPortion, portionsRolling, barProportionRolling, animateTo, offset, user, zoom, showedDead;
const grid = client.grid;
function updateSize() {
let changed = false;
if (canvasWidth != window.innerWidth) {
gameWidth = canvasWidth = offscreenCanvas.width = canvas.width = window.innerWidth;
changed = true;
}
if (canvasHeight != window.innerHeight) {
gameHeight = canvasHeight = offscreenCanvas.height = canvas.height = window.innerHeight;
changed = true;
}
if (changed && user) centerOnPlayer(user, offset);
}
function reset() {
animateGrid = new Grid(consts.GRID_COUNT);
playerPortion = [];
portionsRolling = [];
barProportionRolling = [];
animateTo = [0, 0];
offset = [0, 0];
user = null;
zoom = (Math.min(canvasWidth, canvasHeight) - consts.BORDER_WIDTH) / (consts.CELL_WIDTH * consts.GRID_COUNT);
showedDead = false;
}
reset();
//Paint methods
function paintGridBorder(ctx) {
ctx.fillStyle = "lightgray";
const gridWidth = consts.CELL_WIDTH * consts.GRID_COUNT;
ctx.fillRect(-consts.BORDER_WIDTH, 0, consts.BORDER_WIDTH, gridWidth);
ctx.fillRect(-consts.BORDER_WIDTH, -consts.BORDER_WIDTH, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
ctx.fillRect(gridWidth, 0, consts.BORDER_WIDTH, gridWidth);
ctx.fillRect(-consts.BORDER_WIDTH, gridWidth, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
}
function paintGrid(ctx) {
//Paint background
ctx.fillStyle = "rgb(211, 225, 237)";
ctx.fillRect(0, 0, consts.CELL_WIDTH * consts.GRID_COUNT, consts.CELL_WIDTH * consts.GRID_COUNT);
paintGridBorder(ctx);
//Get viewing limits
const offsetX = (offset[0] - consts.BORDER_WIDTH);
const offsetY = (offset[1] - consts.BORDER_WIDTH);
const minRow = Math.max(Math.floor(offsetY / consts.CELL_WIDTH), 0);
const minCol = Math.max(Math.floor(offsetX / consts.CELL_WIDTH), 0);
const maxRow = Math.min(Math.ceil((offsetY + gameHeight / zoom) / consts.CELL_WIDTH), grid.size);
const maxCol = Math.min(Math.ceil((offsetX + gameWidth / zoom) / consts.CELL_WIDTH), grid.size);
let x, y, animateSpec, baseColor, shadowColor;
//Paint occupied areas (and fading ones)
for (let r = minRow; r < maxRow; r++) {
for (let c = minCol; c < maxCol; c++) {
const p = grid.get(r, c);
x = c * consts.CELL_WIDTH;
y = r * consts.CELL_WIDTH;
animateSpec = animateGrid.get(r, c);
if (client.allowAnimation && animateSpec) {
if (animateSpec.before) { //fading animation
const frac = (animateSpec.frame / ANIMATE_FRAMES);
const back = new Color(.58, .41, .92, 1);
baseColor = animateSpec.before.lightBaseColor.interpolateToString(back, frac);
shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac);
}
else continue;
}
else if (p) {
baseColor = p.lightBaseColor;
shadowColor = p.shadowColor;
}
else continue; //No animation nor is this player owned
const hasBottom = !grid.isOutOfBounds(r + 1, c);
const bottomAnimate = hasBottom && animateGrid.get(r + 1, c);
const totalStatic = !bottomAnimate && !animateSpec;
const bottomEmpty = totalStatic ? (hasBottom && !grid.get(r + 1, c)) : (!bottomAnimate || (bottomAnimate.after && bottomAnimate.before));
if (hasBottom && ((!!bottomAnimate ^ !!animateSpec) || bottomEmpty)) {
ctx.fillStyle = shadowColor.rgbString();
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH + 1, SHADOW_OFFSET);
}
ctx.fillStyle = baseColor.rgbString();
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
}
}
if (!client.allowAnimation) return;
//Paint squares with drop in animation
for (let r = 0; r < grid.size; r++) {
for (let c = 0; c < grid.size; c++) {
animateSpec = animateGrid.get(r, c);
x = c * consts.CELL_WIDTH, y = r * consts.CELL_WIDTH;
if (animateSpec && client.allowAnimation) {
const viewable = r >= minRow && r < maxRow && c >= minCol && c < maxCol;
if (animateSpec.after && viewable) {
//Bouncing the squares.
const offsetBounce = getBounceOffset(animateSpec.frame);
y -= offsetBounce;
shadowColor = animateSpec.after.shadowColor;
baseColor = animateSpec.after.lightBaseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1);
ctx.fillStyle = shadowColor.rgbString();
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH, SHADOW_OFFSET);
ctx.fillStyle = baseColor.rgbString();
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
}
animateSpec.frame++;
if (animateSpec.frame >= ANIMATE_FRAMES) animateGrid.set(r, c, null);
}
}
}
}
function paintUIBar(ctx) {
ctx.fillStyle = "white";
ctx.font = "18px Changa";
//Calcuate rank
const sorted = [];
client.getPlayers().forEach(val => {
sorted.push({player: val, portion: playerPortion[val.num]});
});
sorted.sort((a, b) => {
return (a.portion === b.portion) ? a.player.num - b.player.num : b.portion - a.portion;
});
//Rolling the leaderboard bars
if (sorted.length > 0) {
const maxPortion = sorted[0].portion;
client.getPlayers().forEach(player => {
const rolling = barProportionRolling[player.num];
rolling.value = playerPortion[player.num] / maxPortion;
rolling.update();
});
}
//Show leaderboard
const leaderboardNum = Math.min(consts.LEADERBOARD_NUM, sorted.length);
for (let i = 0; i < leaderboardNum; i++) {
const { player } = sorted[i];
const name = player.name || "Unnamed";
const portion = barProportionRolling[player.num].lag;
const nameWidth = ctx.measureText(name).width;
const barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH);
const barX = canvasWidth - barSize;
const barY = BAR_HEIGHT * i;
const offset = i == 0 ? 10 : 0;
ctx.fillStyle = "rgba(10, 10, 10, .3)";
ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset);
ctx.fillStyle = player.baseColor.rgbString();
ctx.fillRect(barX, barY, barSize, consts.CELL_WIDTH);
ctx.fillStyle = player.shadowColor.rgbString();
ctx.fillRect(barX, barY + consts.CELL_WIDTH, barSize, SHADOW_OFFSET);
ctx.fillStyle = "black";
ctx.fillText(name, barX - nameWidth - 15, barY + 27);
const percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%";
ctx.fillStyle = "white";
ctx.fillText(percentage, barX + 5, barY + consts.CELL_WIDTH - 5);
}
}
function paint(ctx) {
ctx.fillStyle = "#e2ebf3"; //"whitesmoke";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
//Move grid to viewport as said with the offsets, below the stats
ctx.save();
//ctx.translate(0, BAR_HEIGHT);
ctx.beginPath();
ctx.rect(0, 0, gameWidth, gameHeight);
ctx.clip();
//Zoom in/out based on player stats
ctx.scale(zoom, zoom);
ctx.translate(consts.BORDER_WIDTH, consts.BORDER_WIDTH);
paintGrid(ctx);
client.getPlayers().forEach(p => {
const fr = p.waitLag;
if (fr < ANIMATE_FRAMES) p.render(ctx, fr / ANIMATE_FRAMES);
else p.render(ctx);
});
//Reset transform to paint fixed UI elements
ctx.restore();
paintUIBar(ctx);
if ((!user || user.dead) && !showedDead) {
showedDead = true;
console.log("You died!");
}
}
function paintDoubleBuff() {
paint(offctx);
ctx.drawImage(offscreenCanvas, 0, 0);
}
function update() {
updateSize();
//Change grid offsets
for (let i = 0; i <= 1; i++) {
if (animateTo[i] !== offset[i]) {
if (client.allowAnimation) {
const delta = animateTo[i] - offset[i];
const dir = Math.sign(delta);
const mag = Math.min(consts.SPEED, Math.abs(delta));
offset[i] += dir * mag;
}
else offset[i] = animateTo[i];
}
}
//Calculate player portions
client.getPlayers().forEach(player => {
const roll = portionsRolling[player.num];
roll.value = playerPortion[player.num] / consts.GRID_COUNT / consts.GRID_COUNT;
roll.update();
});
}
//Helper methods
function centerOnPlayer(player, pos) {
player = {
posX: 0,
posY: 0
}
const xOff = Math.floor(player.posX - (gameWidth / zoom - consts.CELL_WIDTH) / 2);
const yOff = Math.floor(player.posY - (gameHeight / zoom - consts.CELL_WIDTH) / 2);
const gridWidth = grid.size * consts.CELL_WIDTH + consts.BORDER_WIDTH * 2;
pos[0] = xOff; //Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0);
pos[1] = yOff; //Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0);
}
function getBounceOffset(frame) {
let offsetBounce = ANIMATE_FRAMES;
let bounceNum = BOUNCE_FRAMES.length - 1;
while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum]) {
offsetBounce -= BOUNCE_FRAMES[bounceNum];
bounceNum--;
}
if (bounceNum === -1) return (offsetBounce - frame) * DROP_SPEED;
else {
offsetBounce -= BOUNCE_FRAMES[bounceNum];
frame = frame - offsetBounce;
const midFrame = BOUNCE_FRAMES[bounceNum] / 2;
return (frame >= midFrame) ? (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED : frame * DROP_SPEED;
}
}
function Rolling(value, frames) {
let lag = 0;
if (!frames) frames = 24;
this.value = value;
Object.defineProperty(this, "lag", {
get: function() {
return lag;
},
enumerable: true
});
this.update = function() {
const delta = this.value - lag;
const dir = Math.sign(delta);
const speed = Math.abs(delta) / frames;
const mag = Math.min(Math.abs(speed), Math.abs(delta));
lag += mag * dir;
return lag;
}
}
export default {
addPlayer: function(player) {
playerPortion[player.num] = 0;
portionsRolling[player.num] = new Rolling(9 / consts.GRID_COUNT / consts.GRID_COUNT, ANIMATE_FRAMES);
barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES);
},
disconnect: function() {
},
removePlayer: function(player) {
delete playerPortion[player.num];
delete portionsRolling[player.num];
delete barProportionRolling[player.num];
},
setUser: function(player) {
user = player;
centerOnPlayer(user, offset);
},
reset: reset,
updateGrid: function(row, col, before, after) {
//Keep track of areas
if (before) playerPortion[before.num]--;
if (after) playerPortion[after.num]++;
//Queue animation
if (before === after || !client.allowAnimation) return;
animateGrid.set(row, col, {
before: before,
after: after,
frame: 0
});
},
paint: paintDoubleBuff,
update: update
};

375
src/mode/player.js Normal file
View File

@@ -0,0 +1,375 @@
import jquery from "jquery";
import { Color, Grid } from "../core";
import * as client from "../game-client";
import { consts } from "../../config.js";
const SHADOW_OFFSET = 5;
const ANIMATE_FRAMES = 24;
const BOUNCE_FRAMES = [8, 4];
const DROP_HEIGHT = 24;
const DROP_SPEED = 2;
const MIN_BAR_WIDTH = 65;
const BAR_HEIGHT = SHADOW_OFFSET + consts.CELL_WIDTH;
const BAR_WIDTH = 400;
let canvas, ctx, offscreenCanvas, offctx, canvasWidth, canvasHeight, gameWidth, gameHeight;
const $ = jquery;
$(() => {
canvas = $("#main-ui")[0];
ctx = canvas.getContext("2d");
offscreenCanvas = document.createElement("canvas");
offctx = offscreenCanvas.getContext("2d");
updateSize();
});
let animateGrid, playerPortion, portionsRolling, barProportionRolling, animateTo, offset, user, zoom, showedDead;
const grid = client.grid;
function updateSize() {
let changed = false;
if (canvasWidth != window.innerWidth) {
gameWidth = canvasWidth = offscreenCanvas.width = canvas.width = window.innerWidth;
changed = true;
}
if (canvasHeight != window.innerHeight) {
canvasHeight = offscreenCanvas.height = canvas.height = window.innerHeight;
gameHeight = canvasHeight - BAR_HEIGHT;
changed = true;
}
if (changed && user) centerOnPlayer(user, offset);
}
function reset() {
animateGrid = new Grid(consts.GRID_COUNT);
playerPortion = [];
portionsRolling = [];
barProportionRolling = [];
animateTo = [0, 0];
offset = [0, 0];
user = null;
zoom = 1;
showedDead = false;
}
reset();
//Paint methods
function paintGridBorder(ctx) {
ctx.fillStyle = "lightgray";
const gridWidth = consts.CELL_WIDTH * consts.GRID_COUNT;
ctx.fillRect(-consts.BORDER_WIDTH, 0, consts.BORDER_WIDTH, gridWidth);
ctx.fillRect(-consts.BORDER_WIDTH, -consts.BORDER_WIDTH, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
ctx.fillRect(gridWidth, 0, consts.BORDER_WIDTH, gridWidth);
ctx.fillRect(-consts.BORDER_WIDTH, gridWidth, gridWidth + consts.BORDER_WIDTH * 2, consts.BORDER_WIDTH);
}
function paintGrid(ctx) {
//Paint background
ctx.fillStyle = "rgb(211, 225, 237)";
ctx.fillRect(0, 0, consts.CELL_WIDTH * consts.GRID_COUNT, consts.CELL_WIDTH * consts.GRID_COUNT);
paintGridBorder(ctx);
//Get viewing limits
const offsetX = (offset[0] - consts.BORDER_WIDTH);
const offsetY = (offset[1] - consts.BORDER_WIDTH);
const minRow = Math.max(Math.floor(offsetY / consts.CELL_WIDTH), 0);
const minCol = Math.max(Math.floor(offsetX / consts.CELL_WIDTH), 0);
const maxRow = Math.min(Math.ceil((offsetY + gameHeight / zoom) / consts.CELL_WIDTH), grid.size);
const maxCol = Math.min(Math.ceil((offsetX + gameWidth / zoom) / consts.CELL_WIDTH), grid.size);
let x, y, animateSpec, baseColor, shadowColor;
//Paint occupied areas (and fading ones)
for (let r = minRow; r < maxRow; r++) {
for (let c = minCol; c < maxCol; c++) {
const p = grid.get(r, c);
x = c * consts.CELL_WIDTH;
y = r * consts.CELL_WIDTH;
animateSpec = animateGrid.get(r, c);
if (client.allowAnimation && animateSpec) {
if (animateSpec.before) { //fading animation
const frac = (animateSpec.frame / ANIMATE_FRAMES);
const back = new Color(.58, .41, .92, 1);
baseColor = animateSpec.before.lightBaseColor.interpolateToString(back, frac);
shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac);
}
else continue;
}
else if (p) {
baseColor = p.lightBaseColor;
shadowColor = p.shadowColor;
}
else continue; //No animation nor is this player owned
const hasBottom = !grid.isOutOfBounds(r + 1, c);
const bottomAnimate = hasBottom && animateGrid.get(r + 1, c);
const totalStatic = !bottomAnimate && !animateSpec;
const bottomEmpty = totalStatic ? (hasBottom && !grid.get(r + 1, c)) : (!bottomAnimate || (bottomAnimate.after && bottomAnimate.before));
if (hasBottom && ((!!bottomAnimate ^ !!animateSpec) || bottomEmpty)) {
ctx.fillStyle = shadowColor.rgbString();
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH + 1, SHADOW_OFFSET);
}
ctx.fillStyle = baseColor.rgbString();
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
}
}
if (!client.allowAnimation) return;
//Paint squares with drop in animation
for (let r = 0; r < grid.size; r++) {
for (let c = 0; c < grid.size; c++) {
animateSpec = animateGrid.get(r, c);
x = c * consts.CELL_WIDTH, y = r * consts.CELL_WIDTH;
if (animateSpec && client.allowAnimation) {
const viewable = r >= minRow && r < maxRow && c >= minCol && c < maxCol;
if (animateSpec.after && viewable) {
//Bouncing the squares.
const offsetBounce = getBounceOffset(animateSpec.frame);
y -= offsetBounce;
shadowColor = animateSpec.after.shadowColor;
baseColor = animateSpec.after.lightBaseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1);
ctx.fillStyle = shadowColor.rgbString();
ctx.fillRect(x, y + consts.CELL_WIDTH, consts.CELL_WIDTH, SHADOW_OFFSET);
ctx.fillStyle = baseColor.rgbString();
ctx.fillRect(x, y, consts.CELL_WIDTH + 1, consts.CELL_WIDTH + 1);
}
animateSpec.frame++;
if (animateSpec.frame >= ANIMATE_FRAMES) animateGrid.set(r, c, null);
}
}
}
}
function paintUIBar(ctx) {
//UI Bar background
ctx.fillStyle = "#24422c";
ctx.fillRect(0, 0, canvasWidth, BAR_HEIGHT);
let barOffset;
ctx.fillStyle = "white";
ctx.font = "24px Changa";
barOffset = (user && user.name) ? (ctx.measureText(user.name).width + 20) : 0;
ctx.fillText(user ? user.name : "", 5, consts.CELL_WIDTH - 5);
//Draw filled bar
ctx.fillStyle = "rgba(180, 180, 180, .3)";
ctx.fillRect(barOffset, 0, BAR_WIDTH, BAR_HEIGHT);
const userPortions = portionsRolling[user.num] ? portionsRolling[user.num].lag : 0;
let barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * userPortions + MIN_BAR_WIDTH);
ctx.fillStyle = user ? user.baseColor.rgbString() : "";
ctx.fillRect(barOffset, 0, barSize, consts.CELL_WIDTH);
ctx.fillStyle = user ? user.shadowColor.rgbString() : "";
ctx.fillRect(barOffset, consts.CELL_WIDTH, barSize, SHADOW_OFFSET);
//TODO: dont reset kill count and zoom when we request frames.
//Percentage
ctx.fillStyle = "white";
ctx.font = "18px Changa";
ctx.fillText((userPortions * 100).toFixed(3) + "%", 5 + barOffset, consts.CELL_WIDTH - 5);
//Number of kills
const killsText = "Kills: " + client.getKills();
const killsOffset = 20 + BAR_WIDTH + barOffset;
ctx.fillText(killsText, killsOffset, consts.CELL_WIDTH - 5);
//Calcuate rank
const sorted = [];
client.getPlayers().forEach(val => {
sorted.push({player: val, portion: playerPortion[val.num]});
});
sorted.sort((a, b) => {
return (a.portion === b.portion) ? a.player.num - b.player.num : b.portion - a.portion;
});
const rank = sorted.findIndex(val => val.player === user);
ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length,
ctx.measureText(killsText).width + killsOffset + 20, consts.CELL_WIDTH - 5);
//Rolling the leaderboard bars
if (sorted.length > 0) {
const maxPortion = sorted[0].portion;
client.getPlayers().forEach(player => {
const rolling = barProportionRolling[player.num];
rolling.value = playerPortion[player.num] / maxPortion;
rolling.update();
});
}
//Show leaderboard
const leaderboardNum = Math.min(consts.LEADERBOARD_NUM, sorted.length);
for (let i = 0; i < leaderboardNum; i++) {
const { player } = sorted[i];
const name = player.name || "Unnamed";
const portion = barProportionRolling[player.num].lag;
const nameWidth = ctx.measureText(name).width;
barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH);
const barX = canvasWidth - barSize;
const barY = BAR_HEIGHT * (i + 1);
const offset = i == 0 ? 10 : 0;
ctx.fillStyle = "rgba(10, 10, 10, .3)";
ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset);
ctx.fillStyle = player.baseColor.rgbString();
ctx.fillRect(barX, barY, barSize, consts.CELL_WIDTH);
ctx.fillStyle = player.shadowColor.rgbString();
ctx.fillRect(barX, barY + consts.CELL_WIDTH, barSize, SHADOW_OFFSET);
ctx.fillStyle = "black";
ctx.fillText(name, barX - nameWidth - 15, barY + 27);
const percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%";
ctx.fillStyle = "white";
ctx.fillText(percentage, barX + 5, barY + consts.CELL_WIDTH - 5);
}
}
function paint(ctx) {
ctx.fillStyle = "#e2ebf3"; //"whitesmoke";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
//Move grid to viewport as said with the offsets, below the stats
ctx.save();
ctx.translate(0, BAR_HEIGHT);
ctx.beginPath();
ctx.rect(0, 0, gameWidth, gameHeight);
ctx.clip();
//Zoom in/out based on player stats
ctx.scale(zoom, zoom);
ctx.translate(-offset[0] + consts.BORDER_WIDTH, -offset[1] + consts.BORDER_WIDTH);
paintGrid(ctx);
client.getPlayers().forEach(p => {
const fr = p.waitLag;
if (fr < ANIMATE_FRAMES) p.render(ctx, fr / ANIMATE_FRAMES);
else p.render(ctx);
});
//Reset transform to paint fixed UI elements
ctx.restore();
paintUIBar(ctx);
if ((!user || user.dead) && !showedDead) {
showedDead = true;
console.log("You died!");
}
}
function paintDoubleBuff() {
paint(offctx);
ctx.drawImage(offscreenCanvas, 0, 0);
}
function update() {
updateSize();
//Change grid offsets
for (let i = 0; i <= 1; i++) {
if (animateTo[i] !== offset[i]) {
if (client.allowAnimation) {
const delta = animateTo[i] - offset[i];
const dir = Math.sign(delta);
const mag = Math.min(consts.SPEED, Math.abs(delta));
offset[i] += dir * mag;
}
else offset[i] = animateTo[i];
}
}
//Calculate player portions
client.getPlayers().forEach(player => {
const roll = portionsRolling[player.num];
roll.value = playerPortion[player.num] / consts.GRID_COUNT / consts.GRID_COUNT;
roll.update();
});
//Zoom goes from 1 to .5, decreasing as portion goes up. TODO: maybe can modify this?
if (portionsRolling[user.num]) zoom = 1 / (portionsRolling[user.num].lag + 1);
//TODO: animate player is dead. (maybe explosion?), and tail rewinds itself.
if (user) centerOnPlayer(user, animateTo);
}
//Helper methods
function centerOnPlayer(player, pos) {
const xOff = Math.floor(player.posX - (gameWidth / zoom - consts.CELL_WIDTH) / 2);
const yOff = Math.floor(player.posY - (gameHeight / zoom - consts.CELL_WIDTH) / 2);
const gridWidth = grid.size * consts.CELL_WIDTH + consts.BORDER_WIDTH * 2;
pos[0] = xOff; //Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0);
pos[1] = yOff; //Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0);
}
function getBounceOffset(frame) {
let offsetBounce = ANIMATE_FRAMES;
let bounceNum = BOUNCE_FRAMES.length - 1;
while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum]) {
offsetBounce -= BOUNCE_FRAMES[bounceNum];
bounceNum--;
}
if (bounceNum === -1) return (offsetBounce - frame) * DROP_SPEED;
else {
offsetBounce -= BOUNCE_FRAMES[bounceNum];
frame = frame - offsetBounce;
const midFrame = BOUNCE_FRAMES[bounceNum] / 2;
return (frame >= midFrame) ? (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED : frame * DROP_SPEED;
}
}
function Rolling(value, frames) {
let lag = 0;
if (!frames) frames = 24;
this.value = value;
Object.defineProperty(this, "lag", {
get: function() {
return lag;
},
enumerable: true
});
this.update = function() {
const delta = this.value - lag;
const dir = Math.sign(delta);
const speed = Math.abs(delta) / frames;
const mag = Math.min(Math.abs(speed), Math.abs(delta));
lag += mag * dir;
return lag;
}
}
export function addPlayer(player) {
playerPortion[player.num] = 0;
portionsRolling[player.num] = new Rolling(9 / consts.GRID_COUNT / consts.GRID_COUNT, ANIMATE_FRAMES);
barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES);
};
export function disconnect() {
$("#wasted").fadeIn(1000);
};
export function removePlayer(player) {
delete playerPortion[player.num];
delete portionsRolling[player.num];
delete barProportionRolling[player.num];
};
export function setUser(player) {
user = player;
centerOnPlayer(user, offset);
};
export { reset };
export function updateGrid (row, col, before, after) {
//Keep track of areas
if (before) playerPortion[before.num]--;
if (after) playerPortion[after.num]++;
//Queue animation
if (before === after || !client.allowAnimation) return;
animateGrid.set(row, col, {
before: before,
after: after,
frame: 0
});
};
export { paintDoubleBuff as paint };
export { update };