This commit is contained in:
simon 2020-01-13 21:43:47 -05:00
parent 20abc7e978
commit d467bf0702
7 changed files with 858 additions and 108 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
.idea/ .idea/
*.iml
__pychache__ __pychache__
*.pyc *.pyc
*.log

210
app.py
View File

@ -1,97 +1,175 @@
from threading import Lock import logging
from flask import Flask, request, session from flask import Flask, request, send_file, session, Response
from flask_socketio import Namespace, join_room, leave_room, SocketIO, emit, rooms, close_room, disconnect from flask_socketio import Namespace, SocketIO, emit, join_room
from common import config from common import config, db
from models import BingoGame, GameState, GameMode, User
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!' app.config['SECRET_KEY'] = config["FLASK_SECRET"]
# TODO: test mode mode "threading", "eventlet" or "gevent" socketio = SocketIO(app, async_mode="eventlet")
socketio = SocketIO(app, async_mode=None)
logger = logging.getLogger("default")
# TODO: alphanum room
# TODO: alphanum name w/max len
# TODO: wth is that?!
thread = None
thread_lock = Lock()
@app.route("/") @app.route("/")
def page_index(): def page_index():
return "Hello, world" return send_file("web/index.html")
def background_thread(): @app.route("/<room>")
"""Example of how to send server generated events to clients.""" def play(room):
count = 0 return send_file("web/room.html")
while True:
socketio.sleep(10)
count += 1
socketio.emit('my_response',
{'data': 'Server generated event', 'count': count},
namespace='/test')
# When class-based namespaces are used, any events received by the server are dispatched to a method named as the def log(message, contents, room=None):
# event name with the on_ prefix. For example, event my_event will be handled by a method named on_my_event if config["VERBOSE"]:
logger.info("<%s|%s> [%s] %s:%s" % (
request.remote_addr, request.user_agent, room if room else "~", message, str(contents)))
else:
logger.info("[%s] %s:%s" % (room if room else "~", message, str(contents)))
class BingoNamespace(Namespace): class BingoNamespace(Namespace):
def __init__(self):
super().__init__("/socket")
def on_my_event(self, message): def on_get_end_message(self):
session['receive_count'] = session.get('receive_count', 0) + 1 log("get_end_message", {})
emit('my_response', emit("end_message", {
{'data': message['data'], 'count': session['receive_count']}) "text": "Game has ended, replay?"
})
def on_my_broadcast_event(self, message): def on_cell_click(self, message):
session['receive_count'] = session.get('receive_count', 0) + 1 room = message["room"]
emit('my_response', log("cell_click", message, room)
{'data': message['data'], 'count': session['receive_count']},
broadcast=True) user = db.get_user(message["oid"])
card = db.get_card(message["card"])
cell = card.cells[message["cidx"]]
if not cell.checked or card.last_cell == message["cidx"]:
cell.checked = not cell.checked
card.last_cell = message["cidx"]
db.save_card(card)
emit("card_state", {
"card": card.serialize()
}, room=room)
if card.moves_until_win() == 0:
game = db.get_game(room)
game.winners.append(user.oid)
if game.should_end():
game.state = GameState.ENDED
emit("game_state", {"state": game.state.name})
db.save_game(game)
def on_get_card(self, message):
room = message["room"]
log("get_card", message, room)
user = db.get_user(message["oid"])
game = db.get_game(room)
if room in user.cards:
card = db.get_card(user.cards[room])
else:
card = game.generate_card()
user.cards[room] = card.oid
db.save_card(card)
db.save_user(user)
emit("card_state", {
"card": card.serialize(),
"parent": user.name
}, room=room)
emit("get_card_rsp", {
"card": card.serialize(),
"parent": user.name
})
for player in game.players:
if player != user.oid:
other_user = db.get_user(player)
if room in other_user.cards:
other_card = db.get_card(other_user.cards[room])
emit("card_state", {"card": other_card.serialize(), "parent": other_user.name})
def on_create_game(self, message):
room = message["room"]
log("create_game", message, room)
game = db.get_game(room)
if game.state is GameState.CREATING:
game.state = GameState.PLAYING
game.mode = GameMode[message["mode"]]
game.pool = message["pool"]
db.save_game(game)
emit("game_state", {
"state": game.state.name,
}, room=room)
emit("create_game_rsp", {
"created": True,
})
def on_join(self, message): def on_join(self, message):
join_room(message['room']) log("join", message)
session['receive_count'] = session.get('receive_count', 0) + 1
emit('my_response',
{'data': 'In rooms: ' + ', '.join(rooms()),
'count': session['receive_count']})
def on_leave(self, message): room = message["room"]
leave_room(message['room'])
session['receive_count'] = session.get('receive_count', 0) + 1
emit('my_response',
{'data': 'In rooms: ' + ', '.join(rooms()),
'count': session['receive_count']})
def on_close_room(self, message): user = None
session['receive_count'] = session.get('receive_count', 0) + 1 if "oid" in message:
emit('my_response', {'data': 'Room ' + message['room'] + ' is closing.', user = db.get_user(message["oid"])
'count': session['receive_count']}, if not user:
room=message['room']) emit("join_rsp", {
close_room(message['room']) "ok": False
})
return
def on_my_room_event(self, message): if not user:
session['receive_count'] = session.get('receive_count', 0) + 1 user = User(name=message["name"])
emit('my_response', db.save_user(user)
{'data': message['data'], 'count': session['receive_count']}, session["user"] = user.oid
room=message['room'])
def on_disconnect_request(self): game = db.get_game(message["room"])
session['receive_count'] = session.get('receive_count', 0) + 1 if not game:
emit('my_response', game = BingoGame(room, user.oid)
{'data': 'Disconnected!', 'count': session['receive_count']})
disconnect()
def on_my_ping(self): join_room(room)
emit('my_pong') game.players.add(user.oid)
db.save_game(game)
# TODO: Is this useful?
emit("room_join", {
"name": user.name
}, room=room)
emit("join_rsp", {
"ok": True,
"state": game.state.name,
"oid": user.oid
})
def on_connect(self): def on_connect(self):
global thread pass
with thread_lock:
if thread is None:
thread = socketio.start_background_task(background_thread)
emit('my_response', {'data': 'Connected', 'count': 0})
def on_disconnect(self): def on_disconnect(self):
print('Client disconnected', request.sid) pass
socketio.on_namespace(BingoNamespace())
db.flush()
if __name__ == "__main__": if __name__ == "__main__":
socketio.run( socketio.run(

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,7 +1,24 @@
import logging
import sys
from logging import FileHandler, StreamHandler
from models import DB
config = { config = {
"API_PORT": 3000, "API_PORT": 3000,
"API_HOST": "0.0.0.0", "API_HOST": "0.0.0.0",
"REDIS_HOST": "localhost", "REDIS_HOST": "localhost",
"REDIS_PORT": 6379, "REDIS_PORT": 6379,
"FLASK_SECRET": "secret!",
"VERBOSE": False,
} }
logger = logging.getLogger("default")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s')
file_handler = FileHandler("bingo.log")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(StreamHandler(sys.stdout))
db = DB()

185
models.py
View File

@ -1,8 +1,11 @@
from enum import Enum
from uuid import uuid4 from uuid import uuid4
import random
import math
import redis import redis
import json import json
from common import config import common
class BingoCell: class BingoCell:
@ -15,6 +18,9 @@ class BingoCell:
def serialize(self): def serialize(self):
return self.__dict__ return self.__dict__
def __repr__(self):
return self.text
@staticmethod @staticmethod
def deserialize(j): def deserialize(j):
return BingoCell( return BingoCell(
@ -24,67 +30,184 @@ class BingoCell:
) )
class Row:
class BingoCard: class BingoCard:
def __init__(self, size, cells=None, oid=None): def __init__(self, size, cells=None, oid=None, last_cell=None):
if cells is None: if cells is None:
self.cells = [] cells = []
else: self.cells = cells
self.cells = cells
if oid is None: if oid is None:
self.oid = uuid4().hex oid = uuid4().hex
else: self.oid = oid
self.oid = oid
self.size = size self.size = size
self.last_cell = last_cell
def serialize(self): def serialize(self):
return { return {
"oid": self.oid, "oid": self.oid,
"cells": [c.serialize() for c in self.cells] "cells": tuple(c.serialize() for c in self.cells),
"size": self.size,
"last_cell": self.last_cell,
"moves_until_win": self.moves_until_win(),
} }
def __getitem__(self, col): def moves_until_win(self):
return min(
*(sum(1 for c in self._row(row) if not c.checked) for row in range(0, self.size)),
*(sum(1 for c in self._col(col) if not c.checked) for col in range(0, self.size)),
sum(1 for c in self._diag_left() if not c.checked),
sum(1 for c in self._diag_right() if not c.checked),
)
def _row(self, idx):
return self.cells[idx * self.size:idx * self.size + self.size]
def _col(self, idx):
return [self.cells[c] for c in range(0, len(self.cells)) if c % self.size == idx]
def _diag_left(self):
return [self.cells[c] for c in range(0, len(self.cells), self.size + 1)]
def _diag_right(self):
return [self.cells[c] for c in range(self.size - 1, len(self.cells) - 1, self.size - 1)]
@staticmethod @staticmethod
def deserialize(text): def deserialize(j):
j = json.loads(text) return BingoCard(
return BingoCard(cells=[ cells=tuple(BingoCell.deserialize(c) for c in j["cells"]),
BingoCell.deserialize(c) for c in j["cells"] size=j["size"],
]) oid=j["oid"],
last_cell=j["last_cell"]
)
class GameMode(Enum):
FREE = "free"
class GameState(Enum):
CREATING = "creating"
PLAYING = "playing"
ENDED = "ended"
class BingoGame:
def __init__(self, room, admin, mode=GameMode.FREE, pool=None, state=GameState.CREATING,
players=None, winners=None):
self.room = room
self.mode = mode
self.admin = admin
if pool is None:
pool = []
self.pool = pool
self.state = state
if players is None:
players = set()
self.players = players
if winners is None:
winners = []
self.winners = winners
def should_end(self):
# TODO: add winner count
return len(self.winners) > 0
def generate_card(self):
# TODO: customizable maximum size
size = math.floor(math.sqrt(len(self.pool)))
items = random.sample(self.pool, k=size * size)
return BingoCard(size, cells=[BingoCell(x) for x in items])
def serialize(self):
return {
"room": self.room,
"mode": self.mode.name,
"admin": self.admin,
"state": self.state.name,
"pool": self.pool,
"players": list(self.players),
"winners": self.winners,
}
@staticmethod
def deserialize(j):
return BingoGame(
room=j["room"],
mode=GameMode[j["mode"]],
pool=j["pool"],
admin=j["admin"],
state=GameState[j["state"]],
players=set(j["players"]),
winners=j["winners"]
)
class User:
def __init__(self, name, oid=None, cards=None):
if cards is None:
cards = {}
if oid is None:
oid = uuid4().hex
self.name = name
self.oid = oid
self.cards = cards
@staticmethod
def deserialize(j):
return User(
name=j["name"],
oid=j["oid"],
cards=j["cards"],
)
def serialize(self):
return self.__dict__
class DB: class DB:
_prefix = "bingo:" _prefix = "bingo:"
def __init__(self): def __init__(self):
self._rdb = redis.Redis( self._rdb = redis.Redis(
host=config["REDIS_HOST"], host=common.config["REDIS_HOST"],
port=config["REDIS_PORT"] port=common.config["REDIS_PORT"]
) )
def flush(self): def flush(self):
self._rdb.delete(self._rdb.keys(DB._prefix + "*")) keys = self._rdb.keys(DB._prefix + "*")
if keys:
self._rdb.delete(*keys)
def _get(self, name): def _get(self, name):
return self._rdb.get(DB._prefix + name) text = self._rdb.get(DB._prefix + name)
# print("<GET> %s = %s" % (name, text))
if text:
return json.loads(text)
def _set(self, name, value): def _set(self, name, value):
return self._rdb.set(DB._prefix + name, value) self._rdb.set(DB._prefix + name, json.dumps(value, separators=(",", ":")))
# print("<SET> %s -> %s" % (name, value))
# self._rdb.expire(DB._prefix + name, 3600 * 24 * 14)
def get_card(self, oid): def get_card(self, oid):
return BingoCard.deserialize(self._get(oid)) j = self._get(oid)
if j:
return BingoCard.deserialize(j)
def save_card(self, card): def save_card(self, card):
self._set(card.oid, card.serialize()) self._set(card.oid, card.serialize())
def get_game(self, room):
j = self._get("game:" + room)
if j:
return BingoGame.deserialize(j)
c = BingoCard( def save_game(self, game: BingoGame):
size=4, self._set("game:" + game.room, game.serialize())
cells=[
BingoCell("test") def get_user(self, oid):
]) j = self._get(oid)
print(c.serialize()) if j:
return User.deserialize(j)
def save_user(self, user):
self._set(user.oid, user.serialize())

View File

@ -2,12 +2,67 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Test</title> <title>TODO: title</title>
<style>
body, html {
text-align: center;
padding: 0;
margin: 0;
}
#room-prompt {
height: 50%;
overflow: hidden;
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
input {
line-height: 3;
padding: 0.2rem 0.5rem;
}
.info {
float: right;
padding: 0.5rem;
}
</style>
</head> </head>
<body> <body>
<div id="room-prompt">
<h1>Bingo</h1>
<form onsubmit="onSubmit(); return false">
<input id="room-input" type="text" placeholder="Join/create room">
</form>
</div>
<div class="info">
TODO: info
</div>
<script> <script>
function isAlphanumeric(c) {
return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_".indexOf(c) > -1;
}
window.onload = function () {
const input = document.getElementById("room-input");
input.addEventListener("keydown", e => {
if (!isAlphanumeric(e.key) && e.key !== "Backspace" && e.key !== "Enter") {
e.preventDefault();
}
})
}
function onSubmit() {
const room = document.getElementById("room-input").value;
window.location = "/" + room;
}
</script> </script>
</body> </body>
</html> </html>

484
web/room.html Normal file
View File

@ -0,0 +1,484 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
<style>
* {
padding: 0;
margin: 0;
overflow: hidden;
}
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 100px;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 1200px;
}
</style>
</head>
<body>
<div id="game"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
<script src="https://pixijs.download/release/pixi.min.js"></script>
<div id="create-game" class="modal">
<div class="modal-content">
<h3>Create game</h3>
<form onsubmit="return onCreateGameSubmit()">
<label>
Game mode
<select id="game-mode">
<option value="FREE">Free</option>
<option value="ADMIN" disabled>Admin</option>
<option value="CPU" disabled>CPU</option>
</select>
</label>
<br>
<label>
Maximum size
<select id="maximum-size">
<option selected value="0">Automatic</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</label>
<br>
<label>
Word/Phrases (one per line)
<br>
<textarea id="pool" cols="70" rows="10" placeholder="Words/Phrases (one per line)"></textarea>
</label>
<br>
<label>
Middle cell is free
<input id="middle-free" type="checkbox">
</label>
<br>
<input type="submit" value="Create game">
</form>
</div>
</div>
<div id="create-user" class="modal">
<div class="modal-content">
<h3>New player</h3>
<form onsubmit="return onCreateUserSubmit()">
<label>
Name
<input type="text" id="name">
</label>
<br>
<input type="submit" value="Join room">
</form>
</div>
</div>
<script>
const socket = io("/socket");
const ROOM = window.location.pathname.slice(1);
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
const PORTRAIT = window.innerWidth < window.innerHeight;
let CARDS = {};
let COLS, ROWS;
if (PORTRAIT) {
COLS = 3;
ROWS = 2;
} else {
COLS = 2;
ROWS = 3;
}
let NEXT_ROW = [0, 0];
let NEXT_COL = [0, 0];
let NEXT_SIDE = 0;
//TODO: refact/extract to maskInputAlpha() or someth
const input = document.getElementById("name");
input.addEventListener("keydown", e => {
if (!isAlphanumeric(e.key) && e.key !== "Backspace" && e.key !== "Enter") {
e.preventDefault();
}
})
function createGameModal() {
document.getElementById("create-game").style.display = "block";
}
function onCreateGameSubmit() {
const gameMode = document.getElementById("game-mode").value;
const pool = document.getElementById("pool").value;
const maximumSize = document.getElementById("maximum-size").value;
const middleFree = document.getElementById("middle-free").value;
socket.emit("create_game", {
"oid": selfOid(),
"room": ROOM,
"mode": gameMode,
"maximum_size": maximumSize,
"middle_free": middleFree,
"pool": pool.split(/\s+/).map(w => w.trim())
})
return false;
}
function selfOid() {
return localStorage.getItem("oid")
}
function selfName() {
return localStorage.getItem("name")
}
socket.on("message", msg => {
TEXT._display(msg.text, msg.timeout)
})
socket.on("end_message", msg => {
alert(msg.text)
})
socket.on("game_state", msg => {
if (msg.state === "PLAYING") {
document.getElementById("create-game").style.display = "none";
socket.emit("get_card", {
"oid": selfOid(),
"room": ROOM,
})
} else if (msg.state === "ENDED") {
socket.emit("get_end_message")
}
})
socket.on("card_state", msg => {
if (CARDS.hasOwnProperty("SELF")) {
if (CARDS.hasOwnProperty(msg.card.oid)) {
CARDS[msg.card.oid]._update(msg.card.cells)
} else {
// Add other card
let card = new BingoCard(msg.card.oid, msg.parent, XSCALE, YSCALE);
card._update(msg.card.cells)
app.stage.addChild(card);
CARDS[msg.card.oid] = card;
NEXT_SIDE = (Object.keys(CARDS).length - 1) % 2
card.x = (CARD_WIDTH * XSCALE + CARD_PAD) * NEXT_COL[NEXT_SIDE] + CARD_PAD;
card.y = (CARD_HEIGHT * YSCALE + CARD_PAD) * NEXT_ROW[NEXT_SIDE] + CARD_PAD;
if (NEXT_SIDE === 1) {
card.x += WIDTH * (2 / 3) - CARD_PAD / 2
}
if (NEXT_COL[NEXT_SIDE] === COLS - 1) {
NEXT_COL[NEXT_SIDE] = 0;
NEXT_ROW[NEXT_SIDE] += 1;
} else {
NEXT_COL[NEXT_SIDE] += 1;
}
}
}
})
socket.on("get_card_rsp", msg => {
// Add self card
let card = new BingoCard(msg.card.oid, msg.parent, 1.0);
card._update(msg.card.cells)
card.x = WIDTH / 2 - (CARD_WIDTH / 2);
card.y = HEIGHT / 2 - (CARD_HEIGHT / 2);
app.stage.addChild(card);
CARDS[msg.card.oid] = card;
CARDS["SELF"] = card;
})
function createUser() {
document.getElementById("create-user").style.display = "block";
}
function isAlphanumeric(c) {
return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_".indexOf(c) > -1;
}
function onCreateUserSubmit() {
const name = document.getElementById("name").value;
localStorage.setItem("name", name)
socket.emit("join", {
room: ROOM,
name: name
});
return false;
}
socket.on("join_rsp", msg => {
if (msg.ok === false) {
createUser();
return;
}
document.getElementById("create-user").style.display = "none";
localStorage.setItem("oid", msg.oid)
document.title = msg.oid
if (msg.state === "CREATING") {
createGameModal();
} else if (msg.state === "PLAYING") {
socket.emit("get_card", {
"oid": selfOid(),
"room": ROOM,
})
} else if (msg.state === "ENDED") {
socket.emit("get_end_message")
}
});
socket.on("connect", () => {
let oid = selfOid();
if (oid) {
socket.emit("join", {
room: ROOM,
name: selfName(),
oid: oid,
});
} else {
createUser();
}
});
socket.on("room_join", msg => {
console.log(msg);
})
const app = new PIXI.Application(
{antialias: false, width: WIDTH, height: HEIGHT, resolution: 2, resizeTo: window}
);
document.body.appendChild(app.view);
app.renderer.autoDensity = true;
app.renderer.resize(window.innerWidth, window.innerHeight);
let CARD_WIDTH, CARD_HEIGHT;
if (PORTRAIT) {
CARD_WIDTH = 0.65 * WIDTH;
CARD_HEIGHT = (1 / 3) * HEIGHT;
} else {
CARD_WIDTH = (1 / 3) * WIDTH;
CARD_HEIGHT = 0.70 * HEIGHT;
}
const CELL_COLOR = 0x00FF00;
const CELL_COLOR_CHECKED = 0xFF0000;
const CELL_COLOR_HOVER = 0x00FFFF;
const CELL_PAD = 4;
const CARD_PAD = 20;
function makeCell(cell, size, card_oid, xscale, yscale) {
const g = new PIXI.Graphics();
if (xscale === 1) {
g.interactive = true;
g.buttonMode = true;
g.on("mouseover", () => {
g._color = CELL_COLOR_HOVER;
g._update();
})
g.on("mouseout", () => {
g._color = g._baseColor;
g._update();
})
g.on("click", () => {
socket.emit("cell_click", {
"oid": selfOid(),
"cidx": cell.cidx,
"card": card_oid,
"room": ROOM
})
})
}
g._baseColor = cell.checked ? CELL_COLOR_CHECKED : CELL_COLOR;
g._color = g._baseColor;
g._update = function () {
g.clear();
g.beginFill(this._color);
g.drawRect(0, 0,
((xscale * CARD_WIDTH - CELL_PAD) / size - CELL_PAD),
((yscale * CARD_HEIGHT - CELL_PAD) / size - CELL_PAD)
);
g.endFill()
if (g.children.length === 0) {
const maxWidth = g.width - 4;
const maxHeight = g.height - 4;
const text = new PIXI.Text(cell.text,
{
fontFamily: 'Hack',
fontSize: xscale === 1 ? 16 : 8,
fill: 0x000000,
align: "center",
breakWords: true,
wordWrap: true,
wordWrapWidth: maxWidth,
}
);
text.anchor.set(0.5, 0.5)
text.x = g.width / 2;
text.y = g.height / 2;
//TODO: Adjust text size
if (text.height < maxHeight) {
g.addChild(text);
}
}
}
g._update()
return g
}
function BingoCard(oid, parent, xscale = 1, yscale = 1) {
const COLOR = 0x5cafe2;
let g = new PIXI.Graphics();
g.setMatrix(new PIXI.Matrix().scale(xscale, yscale));
g.lineStyle(3, COLOR);
g.drawRect(0, 0, CARD_WIDTH, CARD_HEIGHT);
const text = new PIXI.Text(parent, {
fontFamily: 'Hack',
fontSize: 16,
fill: 0xFFFFFF,
align: "center",
strokeThickness: 3
}
);
text.anchor.set(0.5, 0.35)
text.x = g.width / 2;
text.y = g.height;
g.addChild(text);
g._update = function (cells) {
g.children.forEach(child => {
if (child !== text) {
child.destroy();
}
})
let size = Math.floor(Math.sqrt(cells.length))
for (let col = 0; col < size; col++) {
for (let row = 0; row < size; row++) {
let cidx = col * size + row;
let cell = cells[cidx];
cell.cidx = cidx;
let c = makeCell(cell, size, oid, xscale, yscale)
c.x = (c.width + CELL_PAD) * row + CELL_PAD;
c.y = (c.height + CELL_PAD) * col + CELL_PAD;
g.addChild(c);
}
}
}
return g;
}
function makeText() {
const PAD = 5;
const t = new PIXI.Text("", {
fontFamily: 'Hack',
fontSize: 38,
fill: 0xFFFFFF,
strokeThickness: 2,
align: "left",
breakWords: true,
wordWrap: true,
wordWrapWidth: WIDTH / 3 - PAD * 2,
});
t.x = WIDTH / 2;
t.y = PORTRAIT ? HEIGHT / 2 : HEIGHT / 12;
t.anchor.set(0.5, 0.5)
t._display = function(text, timeout) {
app.stage.children.sort((a,b) => {
return a === t ? 1 : 0;
})
t.text = text
if (t._to) {
window.clearTimeout(t._to);
}
t._to = window.setTimeout(() => {
t.text = ""
}, timeout)
}
return t;
}
let TEXT = makeText();
app.stage.addChild(TEXT);
let XSCALE, YSCALE;
if (PORTRAIT) {
XSCALE = (WIDTH) / ((CARD_WIDTH + CARD_PAD * 2) * COLS + CARD_PAD * 2)
YSCALE = (HEIGHT / 3) / ((CARD_HEIGHT + CARD_PAD * 4) * ROWS + CARD_PAD * 4)
} else {
XSCALE = (WIDTH / 3) / ((CARD_WIDTH + CARD_PAD * 2) * COLS + CARD_PAD * 2)
YSCALE = (HEIGHT) / ((CARD_HEIGHT + CARD_PAD * 4) * ROWS + CARD_PAD * 4)
}
</script>
</body>
</html>