This commit is contained in:
Mimi 2022-11-12 17:28:24 +08:00
parent 0104b6d766
commit fbf7ff07d4
18 changed files with 205 additions and 186 deletions

View File

@ -27,7 +27,7 @@ After cloning this repository, run the follow commands to install dependencies a
npm start
```
You can configure the game by editing `config.json`.
You can configure the game by editing `config.js`.
## Build
@ -35,11 +35,11 @@ You can configure the game by editing `config.json`.
npm run build
```
**WARNING: Remember to build again after editing any file, include `config.json`.**
**WARNING: Remember to build again after editing any file, include `config.js`.**
## Bots
Set `bots` in `config.json` to a non-zero value, or execute the command below:
Set `bots` in `config.js` to a non-zero value, or execute the command below:
```bash
node paper-io-bot.js ws://localhost:8080

15
bot.js
View File

@ -7,9 +7,10 @@ if (process.argv.length < 3) {
//TODO: add weight to the max land area and last land area, and also the number of kills
//TODO: genetic gene pooling
const core = require("./src/core");
const client = require("./src/game-client");
const { consts } = require("./config.json");
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]];
@ -167,7 +168,7 @@ function traverseGrid(dir) {
}
function printGrid() {
const chars = new core.Grid(consts.GRID_COUNT);
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");
@ -249,8 +250,8 @@ function calcFavorability(params) {
return params.portion + params.kills * 50 + params.survival / 100;
}
client.allowAnimation = false;
client.renderer = {
client.setAllowAnimation(false);
client.setRenderer({
addPlayer: function(player) {
playerPortion[player.num] = 0;
},
@ -278,6 +279,6 @@ client.renderer = {
before && playerPortion[before.num]--;
after && playerPortion[after.num]++;
}
};
});
connect();

View File

@ -1,9 +1,13 @@
window.$ = window.jQuery = require("jquery");
const io = require("socket.io-client");
const client = require("./src/game-client");
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.renderer = flag ? require("./src/mode/god") : require("./src/mode/player");
client.setRenderer(flag ? godRenderer : playerRenderer);
client.connectGame("//" + location.host, $("#name").val(), (success, msg) => {
if (success) {
$("#main-ui").fadeIn(1000);

19
config.js Normal file
View File

@ -0,0 +1,19 @@
export const config = {
"dev": true,
"port": 8080,
"bots": 5,
"fps": 30
};
export const consts = {
"GRID_COUNT": 100,
"CELL_WIDTH": 40,
"SPEED": 5,
"BORDER_WIDTH": 20,
"MAX_PLAYERS": 30,
"NEW_PLAYER_LAG": 60,
"LEADERBOARD_NUM": 5,
"PREFIXES": "Angry Baby Crazy Diligent Excited Fat Greedy Hungry Interesting Japanese Kind Little Magic Naïve Old Powerful Quiet Rich Superman THU Undefined Valuable Wifeless Xiangbuchulai Young Zombie",
"NAMES": "Alice Bob Carol Dave Eve Francis Grace Hans Isabella Jason Kate Louis Margaret Nathan Olivia Paul Queen Richard Susan Thomas Uma Vivian Winnie Xander Yasmine Zach"
};

View File

@ -1,16 +0,0 @@
{
"dev": true,
"port": 8080,
"bots": 5,
"consts": {
"GRID_COUNT": 100,
"CELL_WIDTH": 40,
"SPEED": 5,
"BORDER_WIDTH": 20,
"MAX_PLAYERS": 30,
"NEW_PLAYER_LAG": 60,
"LEADERBOARD_NUM": 5,
"PREFIXES": "Angry Baby Crazy Diligent Excited Fat Greedy Hungry Interesting Japanese Kind Little Magic Naïve Old Powerful Quiet Rich Superman THU Undefined Valuable Wifeless Xiangbuchulai Young Zombie",
"NAMES": "Alice Bob Carol Dave Eve Francis Grace Hans Isabella Jason Kate Louis Margaret Nathan Olivia Paul Queen Richard Susan Thomas Uma Vivian Winnie Xander Yasmine Zach"
}
}

View File

@ -4,9 +4,10 @@
"private": true,
"description": "An multiplayer-IO type game (cloned from Paper-IO)",
"main": "server.js",
"type": "module",
"scripts": {
"build": "browserify client.js | terser > public/js/bundle.js",
"build-dev": "watchify client.js -o public/js/bundle.js"
"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": [
@ -21,15 +22,16 @@
},
"homepage": "https://github.com/stevenjoezhang/paper.io",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"jquery": "^3.6.0",
"@fortawesome/fontawesome-free": "^6.2.0",
"jquery": "^3.6.1",
"mimi-server": "^0.0.1",
"socket.io": "^4.0.1",
"socket.io-client": "^4.0.1"
"socket.io": "^4.5.3",
"socket.io-client": "^4.5.3"
},
"devDependencies": {
"browserify": "^17.0.0",
"terser": "^5.7.0",
"watchify": "^4.0.0"
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"rollup": "^2.48.0",
"terser": "^5.15.1"
}
}

View File

@ -3,9 +3,8 @@ if (process.argv.length < 3) {
process.exit(1);
}
const core = require("./src/core");
const client = require("./src/game-client");
const { consts } = require("./config.json");
import * as client from "./src/game-client.js";
import { consts } from "./config.js";
const MOVES = [[-1, 0], [0, 1], [1, 0], [0, -1]];
@ -65,7 +64,7 @@ function update(frame) {
if (grid.get(row, col) === user) {
//When we are inside our territory
claim = [];
weights = [25, 25, 25, 25];
const weights = [25, 25, 25, 25];
weights[dir] = 100;
weights[mod(dir + 2)] = -9999;
@ -115,8 +114,8 @@ function update(frame) {
const length = 4 * Math.random() + 2;
const ccw = 2 * Math.floor(2 * Math.random()) - 1;
turns = [dir, mod(dir + ccw), mod(dir + ccw * 2), mod(dir + ccw * 3)];
lengths = [breadth, length, breadth + 2 * Math.random() + 1, length];
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++) {
@ -131,7 +130,7 @@ function update(frame) {
claim = [];
//We are playing a little bit more cautious when we are outside and have a
//lot of land
weights = [5, 5, 5, 5];
const weights = [5, 5, 5, 5];
weights[dir] = 50;
weights[mod(dir + 2)] = -9999;
@ -187,8 +186,8 @@ function calcFavorability(params) {
return params.portion + params.kills * 50 + params.survival / 100;
}
client.allowAnimation = false;
client.renderer = {
client.setAllowAnimation(false);
client.setRenderer({
addPlayer: function(player) {
playerPortion[player.num] = 0;
},
@ -206,11 +205,11 @@ client.renderer = {
setUser: function(u) {
user = u;
},
update: update,
update,
updateGrid: function(row, col, before, after) {
before && playerPortion[before.num]--;
after && playerPortion[after.num]++;
}
};
});
connect();

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()]
};

View File

@ -1,10 +1,14 @@
// https://github.com/socketio/socket.io/blob/master/examples/chat/index.js
const MiServer = require("mimi-server");
const express = require("express");
const path = require("path");
const { exec, fork } = require("child_process");
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));
const config = require("./config.json");
config.dev ? exec("npm run build-dev") : exec("npm run build");
const port = process.env.PORT || config.port;
@ -14,12 +18,12 @@ const { app, server } = new MiServer({
static: path.join(__dirname, "public")
});
const io = require("socket.io")(server);
const io = new Server(server);
// Routing
app.use("/font", express.static(path.join(__dirname, "node_modules/@fortawesome/fontawesome-free")));
const Game = require("./src/game-server");
import Game from "./src/game-server.js";
const game = new Game();
io.on("connection", socket => {

View File

@ -100,7 +100,7 @@ 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);
i = 0;
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);
@ -117,4 +117,4 @@ Color.possColors = () => {
return possColors;
}
module.exports = Color;
export default Color;

View File

@ -39,4 +39,4 @@ function isOutOfBounds(data, row, col) {
return row < 0 || row >= data.size || col < 0 || col >= data.size;
}
module.exports = Grid;
export default Grid;

View File

@ -1,17 +1,17 @@
const { consts } = require("../../config.json");
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";
exports.Color = require("./color");
exports.Grid = require("./grid");
exports.Player = require("./player");
exports.initPlayer = (grid, player) => {
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);
}
}
};
exports.updateFrame = (grid, players, dead, notifyKill) => {
}
export function updateFrame(grid, players, dead, notifyKill) {
let adead = [];
if (dead instanceof Array) adead = dead;
@ -96,7 +96,7 @@ exports.updateFrame = (grid, players, dead, notifyKill) => {
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);

View File

@ -1,7 +1,7 @@
const Stack = require("./stack");
const Color = require("./color");
const Grid = require("./grid");
const { consts } = require("../../config.json");
import Stack from "./stack.js";
import Color from "./color.js";
import Grid from "./grid.js";
import { consts } from "../../config.js";
function defineGetter(getter) {
return {
@ -415,4 +415,4 @@ function move(data) {
else if (this.posX % consts.CELL_WIDTH === 0 && this.posY % consts.CELL_WIDTH === 0) this.tail.addTail(heading);
}
module.exports = Player;
export default Player;

View File

@ -25,4 +25,4 @@ function Stack(initSize) {
}
});
}
module.exports = Stack;
export default Stack;

View File

@ -1,33 +1,26 @@
const io = require("socket.io-client");
const core = require("./core");
const { consts } = require("../config.json");
import io from "socket.io-client";
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 _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 core.Grid(consts.GRID_COUNT, (row, col, before, after) => {
let _allowAnimation = true;
const grid = new Grid(consts.GRID_COUNT, (row, col, before, after) => {
invokeRenderer("updateGrid", [row, col, before, after]);
});
let renderer;
let mimiRequestAnimationFrame;
let requestAnimationFrame;
try {
if (window && window.document) {
mimiRequestAnimationFrame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| (callback => { window.setTimeout(callback, 1000 / 30) });
}
}
catch (e) {
mimiRequestAnimationFrame = callback => { setTimeout(callback, 1000 / 30) };
requestAnimationFrame = window.requestAnimationFrame;
} catch {
requestAnimationFrame = callback => { setTimeout(callback, 1000 / 30) };
}
//Public API
@ -58,7 +51,7 @@ function connectGame(url, name, callback, flag) {
reset();
//Load players
data.players.forEach(p => {
const pl = new core.Player(grid, p);
const pl = new Player(grid, p);
addPlayer(pl);
});
user = allPlayers[data.num];
@ -156,8 +149,7 @@ function addPlayer(player) {
}
function invokeRenderer(name, args) {
const renderer = exports.renderer;
if (renderer && typeof renderer[name] === "function") renderer[name].apply(exports, args);
if (renderer && typeof renderer[name] === "function") renderer[name].apply(null, args);
}
function processFrame(data) {
@ -177,9 +169,9 @@ function processFrame(data) {
if (data.newPlayers) {
data.newPlayers.forEach(p => {
if (user && p.num === user.num) return;
const pl = new core.Player(grid, p);
const pl = new Player(grid, p);
addPlayer(pl);
core.initPlayer(grid, pl);
initPlayer(grid, pl);
});
}
const found = new Array(players.length);
@ -204,7 +196,7 @@ function processFrame(data) {
locs[p.num] = [p.posX, p.posY, p.waitLag];
}
dirty = true;
mimiRequestAnimationFrame(paintLoop);
requestAnimationFrame(paintLoop);
timeout = setTimeout(() => {
console.warn("Server has timed-out. Disconnecting.");
socket.disconnect();
@ -218,11 +210,11 @@ function paintLoop() {
if (user && user.dead) {
if (timeout) clearTimeout(timeout);
if (deadFrames === 60) { //One second of frame
const before = allowAnimation;
allowAnimation = false;
const before = _allowAnimation;
_allowAnimation = false;
update();
invokeRenderer("paint", []);
allowAnimation = before;
_allowAnimation = before;
user = null;
deadFrames = 0;
return;
@ -231,7 +223,7 @@ function paintLoop() {
deadFrames++;
dirty = true;
update();
mimiRequestAnimationFrame(paintLoop);
requestAnimationFrame(paintLoop);
}
}
@ -240,7 +232,7 @@ function reset() {
grid.reset();
players = [];
allPlayers = [];
kills = 0;
_kills = 0;
invokeRenderer("reset");
}
@ -251,8 +243,8 @@ function setUser(player) {
function update() {
const dead = [];
core.updateFrame(grid, players, dead, (killer, other) => { //addKill
if (players[killer] === user && killer !== other) kills++;
updateFrame(grid, players, dead, (killer, other) => { //addKill
if (players[killer] === user && killer !== other) _kills++;
});
dead.forEach(val => {
console.log((val.name || "Unnamed") + " is dead");
@ -261,30 +253,30 @@ function update() {
});
invokeRenderer("update", [frame]);
}
//Export stuff
[connectGame, changeHeading, getUser, getPlayers, getOthers, disconnect].forEach(f => {
exports[f.name] = f;
});
Object.defineProperties(exports, {
allowAnimation: {
get: function() {
return allowAnimation;
},
set: function(val) {
allowAnimation = !!val;
},
enumerable: true
function setRenderer(r) {
renderer = r;
}
function setAllowAnimation(allow) {
_allowAnimation = allow;
}
// Export stuff
export { connectGame, changeHeading, getUser, getPlayers, getOthers, disconnect, setRenderer, setAllowAnimation };
export const allowAnimation = {
get: function() {
return _allowAnimation;
},
grid: {
get: function() {
return grid;
},
enumerable: true
set: function(val) {
_allowAnimation = !!val;
},
kills: {
get: function() {
return kills;
},
enumerable: true
}
});
enumerable: true
};
export { grid };
export const kills = {
get: function() {
return _kills;
},
enumerable: true
};

View File

@ -1,8 +1,8 @@
const core = require("./core");
const { consts } = require("../config.json");
import { Color, Grid, Player, initPlayer, updateFrame } from "./core/index.js";
import { consts } from "../config.js";
function Game(id) {
const possColors = core.Color.possColors();
const possColors = Color.possColors();
let nextInd = 0;
const players = [];
const gods = [];
@ -10,7 +10,7 @@ function Game(id) {
const frameLocs = [];
let frame = 0;
let filled = 0;
const grid = new core.Grid(consts.GRID_COUNT, (row, col, before, after) => {
const grid = new Grid(consts.GRID_COUNT, (row, col, before, after) => {
if (!!after ^ !!before) {
if (after) filled++;
else filled--;
@ -30,13 +30,13 @@ function Game(id) {
num: nextInd,
base: possColors.shift()
};
const p = new core.Player(grid, params);
const p = new Player(grid, params);
p.tmpHeading = params.currentHeading;
p.client = client;
players.push(p);
newPlayers.push(p);
nextInd++;
core.initPlayer(grid, p);
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;
@ -183,7 +183,7 @@ function Game(id) {
function update() {
const dead = [];
core.updateFrame(grid, players, dead);
updateFrame(grid, players, dead);
for (const p of dead) {
if (!p.handledDead) {
possColors.push(p.baseColor);
@ -236,4 +236,4 @@ function findEmpty(grid) {
}
return (available.length === 0) ? null : available[Math.floor(available.length * Math.random())];
}
module.exports = Game;
export default Game;

View File

@ -1,8 +1,8 @@
/* global $ */
import jquery from "jquery";
const core = require("../core");
const client = require("../game-client");
const { consts } = require("../../config.json");
import { Grid, Color } from "../core";
import * as client from "../game-client";
import { consts } from "../../config.js";
const SHADOW_OFFSET = 5;
const ANIMATE_FRAMES = 24;
@ -14,6 +14,7 @@ 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];
@ -40,7 +41,7 @@ function updateSize() {
}
function reset() {
animateGrid = new core.Grid(consts.GRID_COUNT);
animateGrid = new Grid(consts.GRID_COUNT);
playerPortion = [];
portionsRolling = [];
barProportionRolling = [];
@ -89,7 +90,7 @@ function paintGrid(ctx) {
if (client.allowAnimation && animateSpec) {
if (animateSpec.before) { //fading animation
const frac = (animateSpec.frame / ANIMATE_FRAMES);
const back = new core.Color(.58, .41, .92, 1);
const back = new Color(.58, .41, .92, 1);
baseColor = animateSpec.before.lightBaseColor.interpolateToString(back, frac);
shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac);
}
@ -169,7 +170,7 @@ function paintUIBar(ctx) {
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 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;
@ -298,7 +299,7 @@ function Rolling(value, frames) {
}
}
module.exports = exports = {
export default {
addPlayer: function(player) {
playerPortion[player.num] = 0;
portionsRolling[player.num] = new Rolling(9 / consts.GRID_COUNT / consts.GRID_COUNT, ANIMATE_FRAMES);

View File

@ -1,8 +1,8 @@
/* global $ */
import jquery from "jquery";
const core = require("../core");
const client = require("../game-client");
const { consts } = require("../../config.json");
import { Color, Grid } from "../core";
import * as client from "../game-client";
import { consts } from "../../config.js";
const SHADOW_OFFSET = 5;
const ANIMATE_FRAMES = 24;
@ -14,6 +14,7 @@ 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];
@ -41,7 +42,7 @@ function updateSize() {
}
function reset() {
animateGrid = new core.Grid(consts.GRID_COUNT);
animateGrid = new Grid(consts.GRID_COUNT);
playerPortion = [];
portionsRolling = [];
barProportionRolling = [];
@ -90,7 +91,7 @@ function paintGrid(ctx) {
if (client.allowAnimation && animateSpec) {
if (animateSpec.before) { //fading animation
const frac = (animateSpec.frame / ANIMATE_FRAMES);
const back = new core.Color(.58, .41, .92, 1);
const back = new Color(.58, .41, .92, 1);
baseColor = animateSpec.before.lightBaseColor.interpolateToString(back, frac);
shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac);
}
@ -333,37 +334,42 @@ function Rolling(value, frames) {
}
}
module.exports = exports = {
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() {
$("#wasted").fadeIn(1000);
},
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
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 };