Initial commit

This commit is contained in:
2020-05-05 22:35:48 +02:00
parent 3c4ec2f6dc
commit 9922e5470c
21 changed files with 2019 additions and 0 deletions

42
uno/client/Card.js Normal file
View File

@@ -0,0 +1,42 @@
export class Card {
constructor(color, value, turnedUp) {
this.color = color;
this.value = value;
this.turnedUp = turnedUp;
}
allows(card) {
return this.color === card.color || this.value === card.value;
}
/**
* Draws the card
* @param {Renderer} renderer
*/
draw(renderer, x = 0, y = 0) {
const cardBody = renderer.begin().roundedRect(x, y, 100, 150, 10);
if (this.turnedUp) {
cardBody.fill(this.color).stroke(255).strokeWidth(5).close();
renderer.begin().center(x, y, 100, 150).circle(0, 0, 40).fill(255).close();
switch (this.value) {
case "CHOOSE":
if (!this.actualColor) {
renderer.begin().center(x, y, 100, 150).arc(0, 0, 35, 0, 0.5 * Math.PI).finish().fill('RED').close();
renderer.begin().center(x, y, 100, 150).arc(0, 0, 35, 0.5 * Math.PI, Math.PI).finish().fill('GREEN').close();
renderer.begin().center(x, y, 100, 150).arc(0, 0, 35, Math.PI, 1.5 * Math.PI).finish().fill('YELLOW').close();
renderer.begin().center(x, y, 100, 150).arc(0, 0, 35, 1.5 * Math.PI, 2 * Math.PI).finish().fill('BLUE').close();
} else {
renderer.begin().center(x, y, 100, 150).circle(0, 0, 35).fill(this.actualColor).close();
}
break;
default:
renderer.begin().center(x, y, 100, 150).text(this.value[0]).align('center').fontSize(48).baseline('middle').fill(0).close();
break;
}
} else {
const temp = cardBody.offset.x;
cardBody.fill(150, 0, 255).stroke(255).strokeWidth(5).close();
}
}
}

30
uno/client/CardDeck.js Normal file
View File

@@ -0,0 +1,30 @@
import {
Card
} from "./Card.js";
export class CardDeck {
constructor() {
this.cards = [];
}
/**
* Returns a random card of this deck
* @returns {Card}
*/
getRandomCard() {
return this.cards[Math.floor(Math.random() * this.cards.length)];
}
}
export const cardDecks = [];
// Default card deck
const defaultDeck = new CardDeck();
const colors = ['RED', 'GREEN', 'BLUE', 'YELLOW'];
colors.forEach(color => {
for (let i = 0; i < 10; i++) {
defaultDeck.cards.push(new Card(color, i));
}
});
cardDecks.push(defaultDeck);

View File

@@ -0,0 +1,66 @@
import {
isInRegion,
getRelativeRegionCoordinates,
getAbsoluteRegionCoordinates
} from "./helpers.js";
export class EventHandler {
constructor(element) {
this.element = element;
this.listeners = [];
this.element.addEventListener('mousemove', e => {
const {
x,
y,
relX,
relY,
} = this.getCoordinates(e.clientX, e.clientY);
this.listeners.filter(l => l.event === 'hover' && isInRegion(relX, relY, l.region)).forEach(l => {
const coords = getAbsoluteRegionCoordinates(x, y, l.region, this.element.width, this.element.height);
const relCoords = getRelativeRegionCoordinates(relX, relY, l.region);
const event = {
x: coords.x,
y: coords.y,
relX: relCoords.x,
relY: relCoords.y,
event: e,
};
l.callback(event);
});
});
this.element.addEventListener('click', e => {
const event = {
x: e.clientX - this.element.offsetLeft,
y: e.clientY - this.element.offsetTop,
event: e,
};
this.listeners.filter(l => l.event === 'click').forEach(listener => listener.callback(event));
});
}
getCoordinates(x, y) {
return {
x: x - this.element.offsetLeft,
y: y - this.element.offsetTop,
relX: (x - this.element.offsetLeft) / this.element.clientWidth,
relY: (y - this.element.offsetTop) / this.element.clientHeight,
};
}
on(type, callback, x = 0, y = 0, width = 1, height = 1) {
this.listeners.push({
event: type,
region: {
x,
y,
width,
height
},
callback: callback,
});
}
}

206
uno/client/Game.js Normal file
View File

@@ -0,0 +1,206 @@
import {
cardDecks
} from "./CardDeck.js";
import {
Player
} from "./Player.js";
import {
Stack
} from "./Stack.js";
import {
SocketHandler
} from "./socket.js";
import {
Card
} from "./Card.js";
export class Game {
constructor() {
this.players = [];
this.player = new Player(this);
this.players.push(this.player);
this.activePlayerID = null;
this.players[0].isTurn = true;
this.clockwise = true;
this.deck = cardDecks[0];
this.drawingStack = new Stack();
this.playingStack = new Stack();
this.config = {
startingCards: 10,
};
this.eventHandlers = [];
this.eventListeners = [];
this.arrowRotation = 0;
this.choosingColor = false;
}
init() {
this.player.init();
for (let i = 0; i < 5; i++) {
this.drawingStack.add(new Card("", "", false));
}
this.registerListener('click', (data) => {
if (this.choosingColor) {
return;
}
console.log(data);
const minX = this.renderer.width / 2 - 330;
const maxX = minX + 130;
const minY = this.renderer.height / 2 - 330;
const maxY = minY + 180;
if (data.x > minX && data.x < maxX && data.y > minY && data.y < maxX) {
console.log('found');
this.onCardDrawn();
}
});
this.registerListener('click', (data) => {
if (!this.choosingColor) {
return;
}
const centerX = this.renderer.width / 2;
const centerY = this.renderer.height / 2;
const dx = data.x - centerX;
const dy = data.y - centerY;
const d = Math.sqrt(dx ** 2, dy ** 2)
if (d <= 125) {
if (dx > 0 && dy > 0) {
this.onColorSelected('RED');
} else if (dx > 0 && dy < 0) {
this.onColorSelected('BLUE');
} else if (dx < 0 && dy > 0) {
this.onColorSelected('GREEN');
} else {
this.onColorSelected('YELLOW');
}
}
})
}
registerEventHandler(eventHandler) {
this.eventHandlers.push(eventHandler);
this.eventListeners.forEach(listener => eventHandler.on(...listener));
}
registerListener(type, eventListener, x = 0, y = 0, width = 1, height = 1) {
this.eventListeners.push([
type,
eventListener,
x,
y,
width,
height
]);
this.eventHandlers.forEach(handler => handler.on(type, eventListener), x, y, width, height);
}
onCardPlayed() {}
onCardDrawn() {}
onColorSelected(color) {}
playerJoined(player, cardCount) {
this.players.push(player);
for (let i = 0; i < cardCount; i++) {
player.hand.push(new Card("", "", false));
}
}
playCard(card) {
if (this.activePlayerID === this.player.id) {
this.onCardPlayed(card);
}
}
draw(renderer) {
// Drawing main stack
renderer.push();
renderer.anchor('center', 'center');
this.playingStack.draw(renderer);
renderer.pop();
// Drawing direction
renderer.push();
renderer.anchor('center', 'center');
renderer.rotate(this.arrowRotation);
this.renderer.begin().arc(0, 0, 200, 0, 0.8 * Math.PI).stroke(0).strokeWidth(20).close();
this.renderer.begin().arc(0, 0, 200, Math.PI, 1.8 * Math.PI).stroke(0).strokeWidth(20).close();
if (this.clockwise) {
this.renderer.begin().rotate(0.8, 'RADIANS').translate(200, 0).triangle(-30, 0, 60, 60).fill(0).close();
this.renderer.begin().rotate(1.8, 'RADIANS').translate(200, 0).triangle(-30, 0, 60, 60).fill(0).close();
this.arrowRotation++;
} else {
this.renderer.begin().rotate(0, 'RADIANS').translate(200, 0).triangle(-30, 0, 60, -60).fill(0).close();
this.renderer.begin().rotate(1, 'RADIANS').translate(200, 0).triangle(-30, 0, 60, -60).fill(0).close();
this.arrowRotation--;
}
if (this.arrowRotation > 360) {
this.arrowRotation -= 360;
} else if (this.arrowRotation < 0) {
this.arrowRotation += 360;
}
renderer.pop();
// Drawing backup stack
renderer.push();
renderer.anchor('center', 'center');
renderer.translate(-250, -250);
this.drawingStack.draw(renderer);
renderer.pop();
// Drawing color selection
if (this.choosingColor) {
renderer.push();
renderer.anchor('center', 'center');
renderer.begin().arc(0, 0, 125, 0, 0.5 * Math.PI).finish().fill('RED').close();
renderer.begin().arc(0, 0, 125, 0.5 * Math.PI, Math.PI).finish().fill('GREEN').close();
renderer.begin().arc(0, 0, 125, Math.PI, 1.5 * Math.PI).finish().fill('YELLOW').close();
renderer.begin().arc(0, 0, 125, 1.5 * Math.PI, 2 * Math.PI).finish().fill('BLUE').close();
renderer.pop();
}
// Drawing own hand
renderer.push();
renderer.anchor('center', 'bottom');
if (this.activePlayerID === this.player.id) {
renderer.begin().text("Your turn", 0, -200).fill(0).close();
}
this.player.draw(renderer);
renderer.pop();
// Drawing other players
if (this.players.length > 1) {
renderer.push();
renderer.anchor('right', 'center');
renderer.rotate(-90);
this.players[1].draw(renderer);
renderer.pop();
}
if (this.players.length > 2) {
renderer.push();
renderer.anchor('center', 'top');
renderer.rotate(-180);
renderer.begin().roundedRect(0, 0, 100, 150, 10).fill(0).close();
this.players[2].draw(renderer);
renderer.pop();
}
if (this.players.length > 3) {
renderer.push();
renderer.anchor('left', 'center');
renderer.rotate(90);
renderer.begin().roundedRect(0, 0, 100, 150, 10).fill(0).close();
this.players[3].draw(renderer);
renderer.pop();
}
}
}

90
uno/client/Player.js Normal file
View File

@@ -0,0 +1,90 @@
export class Player {
constructor(game, id, name) {
this.game = game;
this.id = id;
this.name = name;
this.hand = [];
this.isTurn = false;
this.highlightedCard = null;
this.cardArea = {
currentWidth: 0.5,
allowedWidth: 0.5,
currentCard: null,
cardWidth: 0.1,
};
}
init() {
this.registerEventListeners();
}
registerEventListeners() {
this.game.registerListener('hover', event => {
const width = this.cardArea.currentWidth;
const left = 0.5 - width / 2;
const right = left + width - this.cardArea.cardWidth;
this.highlightedCard = Math.min(Math.max(Math.floor((event.relX - left) / (right - left) * this.hand.length), 0), this.hand.length - 1);
}, 0, 0.8, 1, 0.2);
this.game.registerListener('click', event => {
console.log('clicked card');
if (!this.isTurn) {
return;
}
if (this.highlightedCard !== null) {
this.playCard(this.highlightedCard);
}
});
}
addCard(card) {
this.hand.push(card);
}
setHand(cards) {
this.hand = cards
.sort((a, b) => a.value.localeCompare(b.value))
.sort((a, b) => a.color.localeCompare(b.color));
}
playCard(index) {
console.log('played', this.name);
const card = this.hand[index];
this.game.playCard(card);
}
getCardOffset() {
const width = 800;
const offset = Math.min((width - 100) / this.hand.length, 60);
return offset;
}
getWidth(offset) {
return offset * (this.hand.length - 1) + 100;
}
draw(renderer) {
const offset = this.getCardOffset();
const width = this.getWidth(offset);
let x = -width / 2;
this.cardArea.currentWidth = width / this.game.renderer.width;
this.cardArea.cardWidth = 100 / this.game.renderer.width;
this.hand.forEach((card, index) => {
if (this.highlightedCard === index) {
this.cardArea.currentCard = x / this.game.renderer.width;
card.draw(renderer, x);
x += 100;
} else {
card.draw(renderer, x);
x += offset;
}
});
renderer.pop();
}
}

31
uno/client/Stack.js Normal file
View File

@@ -0,0 +1,31 @@
export class Stack {
constructor() {
this.cards = [];
}
allows(card) {
return card.color === 'BLACK' || this.cards.length === 0 || this.cards[this.cards.length - 1].allows(card);
}
add(card) {
this.cards.push(card);
if (this.cards.length > 5) {
this.cards.splice(0, 1);
}
return true;
}
setTopColor(color) {
this.cards[this.cards.length - 1].actualColor = color;
}
draw(renderer) {
if (this.cards.length === 0) {
return;
}
for (let i = 0; i < 5 && i < this.cards.length; i++) {
this.cards[i].draw(renderer, -i * 6, -i * 6);
}
}
}

18
uno/client/helpers.js Normal file
View File

@@ -0,0 +1,18 @@
export function isInRegion(x, y, region) {
return x >= region.x && x <= region.x + region.width &&
y >= region.y && y <= region.y + region.height;
}
export function getRelativeRegionCoordinates(x, y, region) {
return {
x: (x - region.x) / region.width,
y: (y - region.y) / region.height,
};
}
export function getAbsoluteRegionCoordinates(x, y, region, width, height) {
return {
x: 0,
y: 0,
};
}

39
uno/client/index.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UNO</title>
<link rel="stylesheet" href="styles.css" type="text/css">
</head>
<body>
<div class="dialog-container title-screen">
<div class="container">
<h1 class="title margin-bottom">ETT.IO</h1>
<button class="btn primary" id="btnCreateGame">Create New Game</button>
<button class="btn primary margin-bottom" id="btnJoinGame">Join Game</button>
<button class="btn">Credits</button>
</div>
</div>
<div class="dialog-container dialog hidden" id="dialogJoinGame">
<div class="container">
<div class="dialog-content">
<p>Enter a game code:</p>
<input type="text" id="inputGameCode">
<div class="dialog-footer">
<button class="btn primary" id="btnJoinGameConfirm">Join Game</button>
</div>
</div>
</div>
</div>
<canvas id="canvas"></canvas>
<script type="module" src="menu.js"></script>
</body>
</html>

36
uno/client/menu.js Normal file
View File

@@ -0,0 +1,36 @@
import {
triggerCreateGame,
triggerJoinGame,
} from './uno.js';
const btnCreateGame = document.getElementById('btnCreateGame');
const btnJoinGame = document.getElementById('btnJoinGame');
const btnJoinGameConfirm = document.getElementById('btnJoinGameConfirm');
const inputGameCode = document.getElementById('inputGameCode');
btnCreateGame.addEventListener('click', () => {
triggerCreateGame();
fadeOutTitleScreen();
});
btnJoinGame.addEventListener('click', () => {
showJoinDialog();
});
btnJoinGameConfirm.addEventListener('click', () => {
const gameCode = inputGameCode.value;
triggerJoinGame(gameCode);
fadeOutTitleScreen();
closeJoinDialog();
})
function showJoinDialog() {
document.getElementById('dialogJoinGame').classList.remove('hidden');
}
function closeJoinDialog() {
document.getElementById('dialogJoinGame').classList.add('hidden');
}
function fadeOutTitleScreen() {
document.getElementsByClassName('title-screen')[0].classList.add('hidden');
}

574
uno/client/render.js Normal file
View File

@@ -0,0 +1,574 @@
export class Renderer {
constructor(canvas, game) {
this.game = game;
game.renderer = this;
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d');
this.width = canvas.width;
this.height = canvas.height;
this.anchorOption = {
hor: 'left',
ver: 'top'
};
this.virtualWidth = 100;
this.virtualHeight = 100;
}
setSize(width, height) {
this.width = width;
this.height = height;
this.canvas.width = width;
this.canvas.height = height;
}
draw() {
this.ctx.clearRect(0, 0, this.width, this.height);
this.game.draw(this);
window.requestAnimationFrame(() => this.draw());
}
begin() {
const drawing = new Drawing(this);
drawing.anchor(this.anchorOption.hor, this.anchorOption.ver);
return drawing;
}
push() {
this.ctx.save();
}
pop() {
this.ctx.restore();
}
anchor(hor, ver) {
let x = 0;
let y = 0;
switch (hor) {
case 'center':
x = this.width / 2;
break;
case 'right':
x = this.width;
break;
}
switch (ver) {
case 'center':
y = this.height / 2;
break;
case 'bottom':
y = this.height;
}
this.translate(x, y);
this.anchorOption = {
hor,
ver
};
}
rotate(degrees) {
this.ctx.rotate(degrees * Math.PI / 180);
}
translate(x, y) {
this.ctx.translate(x, y);
}
scale(x, y) {
this.ctx.scale(x, y);
}
}
class Drawing {
constructor(renderer) {
this.renderer = renderer;
this.ctx = renderer.ctx;
this.ctx.save();
this.anchorPoint = {
x: 0,
y: 0,
};
this.offset = {
x: 0,
y: 0,
};
this.translation = {
x: 0,
y: 0,
};
this.rotation = 0;
this.fillColor = undefined;
this.strokeColor = undefined;
this.lineWidth = 1;
this.lineCap = 'butt';
this.lineJoin = 'miter';
this.type = undefined;
this.args = {};
this.textOptions = {
font: 'sans-serif',
fontSize: 16,
alignment: 'left',
baseline: 'alphabetic',
};
}
/*
Shapes
*/
arc(x, y, radius, startAngle, endAngle) {
this.type = 'arc';
this.args = {
x,
y,
radius,
startAngle,
endAngle
};
return this;
}
circle(x, y, radius) {
this.type = 'circle';
this.args = {
x,
y,
radius
};
return this;
}
rect(x, y, width, height) {
this.type = 'rect';
this.args = {
x,
y,
width,
height
};
return this;
}
roundedRect(x, y, width, height, roundness) {
this.type = 'roundedRect';
this.args = {
x,
y,
width,
height,
roundness
};
return this;
}
triangle(x, y, width, height) {
this.type = 'triangle';
this.args = {
x1: x,
y1: y,
x2: x + width,
y2: y,
x3: x + width / 2,
y3: y + height
};
return this;
}
/*
Path
*/
vertex(x, y) {
this.type = 'line';
if (!this.args.vertices) {
this.args.vertices = [];
}
this.args.vertices.push([x, y]);
return this;
}
finish() {
this.args.close = true;
return this;
}
/*
Text
*/
text(string, x = 0, y = 0) {
this.type = 'text';
this.args = {
x,
y,
string
};
return this;
}
/**
* Set text alignment
* @param {'start'|'end'|'left'|'center'|'right'} alignment
* @returns {this}
*/
align(alignment) {
this.textOptions.alignment = alignment;
return this;
}
/**
* Sets text baseline
* @param {'alphabetic'|'top'|'hanging'|'middle'|'ideographic'|'bottom'} base
* @return {Drawing}
*/
baseline(base) {
this.textOptions.baseline = base;
return this;
}
fontSize(fontSize) {
this.textOptions.fontSize = fontSize;
return this;
}
/*
Transformations
*/
anchor(hor, ver) {
switch (hor) {
case 'center':
this.anchorPoint.x = this.renderer.width / 2;
this.offset.x = -0.5;
break;
case 'right':
this.anchorPoint.x = this.renderer.width;
this.offset.x = -1;
break;
default:
this.anchorPoint.x = 0;
this.offset.x = 0;
break;
}
switch (ver) {
case 'center':
this.anchorPoint.y = this.renderer.height / 2;
this.offset.y = -0.5;
break;
case 'bottom':
this.anchorPoint.y = this.renderer.height;
this.offset.y = -1;
break;
default:
this.anchorPoint.y = 0;
this.offset.y = 0;
break;
}
return this;
}
rotate(amount, mode = 'DEGREES') {
if (mode === 'DEGREES') {
amount *= 180;
}
this.rotation += amount * Math.PI;
return this;
}
translate(x, y) {
this.translation.x += x;
this.translation.y += y;
return this;
}
/**
* Translate to center of rectangle
* @param {number} x x position
* @param {number} y y position
* @param {number} width width
* @param {number} height height
* @returns {this}
*/
center(x, y, width, height) {
this.translation.x += x + this.offset.x * width + width / 2;
this.translation.y += y + this.offset.y * height + height / 2;
return this;
}
/*
Colors
*/
fill(...color) {
this.fillColor = parseColor('rgb', ...color);
return this;
}
stroke(...color) {
this.strokeColor = parseColor('rgb', ...color);
return this;
}
/**
* Sets line width for strokes
* @param {number} width
*/
strokeWidth(width) {
this.lineWidth = width;
return this;
}
/**
* Sets line cap for strokes
* @param {'butt'|'round'|'square'} style
*/
strokeCap(style) {
this.lineCap = style;
return this;
}
calcCoords(x, y, width, height) {
return [
x + this.offset.x * width,
y + this.offset.y * height,
];
}
close() {
if (this.fillColor) {
this.ctx.fillStyle = this.fillColor;
}
if (this.strokeColor) {
this.ctx.strokeStyle = this.strokeColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.lineCap = this.lineCap;
}
this.ctx.rotate(this.rotation);
this.ctx.translate(this.translation.x, this.translation.y);
switch (this.type) {
case 'arc':
drawArc(this.ctx, this.args, !!this.fillColor, !!this.strokeColor);
break;
case 'circle':
drawCircle(this.ctx, this.args, !!this.fillColor, !!this.strokeColor);
break;
case 'line':
drawShape(this.ctx, this.args.close, this.args.vertices, !!this.fillColor, !!this.strokeColor);
break;
case 'rect':
/*if (this.fillColor) {
this.ctx.fillRect(...this.args);
}
if (this.strokeColor) {
this.ctx.strokeRect(...this.args);
}*/
break;
case 'roundedRect':
this.args.x = this.calcCoords(this.args.x, 0, this.args.width, 0)[0];
this.args.y = this.calcCoords(0, this.args.y, 0, this.args.height)[1];
drawRoundedRect(this.ctx, this.args, !!this.fillColor, !!this.strokeColor);
break;
case 'shape':
console.log('test');
drawShape(this.ctx, true, this.args, !!this.fillColor, !!this.strokeColor);
break;
case 'text':
this.ctx.font = this.textOptions.fontSize + 'px ' + this.textOptions.font;
this.args.x = this.calcCoords(this.args.x, 0, 0)[0];
this.args.y = this.calcCoords(0, this.args.y, 0, 0)[1];
drawText(this.ctx, this.args, !!this.fillColor, !!this.strokeColor, this.textOptions);
break;
case 'triangle':
drawTriangle(this.ctx, this.args, !!this.fillColor, !!this.strokeColor);
break;
}
this.ctx.restore();
}
}
function parseColor(colorMode, ...color) {
if (color.length === 0) {
return '#000000';
}
if (color.length === 1 && typeof color[0] === 'string') {
return color[0];
}
if (color.length === 1 && typeof color[0] === 'number') {
color.push(color[0]);
}
if (color.length === 2 && typeof color[0] === 'number') {
color.push(color[0]);
}
if (color.length === 3) {
return `${colorMode}(${color[0]}, ${color[1]}, ${color[2]})`;
}
return '#000000';
}
function drawArc(ctx, args, fill, stroke) {
const {
x,
y,
radius,
startAngle,
endAngle,
close,
} = args;
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
if (close) {
ctx.lineTo(x, y);
ctx.closePath();
}
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
function drawCircle(ctx, args, fill, stroke) {
const {
x,
y,
radius
} = args;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
function drawShape(ctx, close, vertices, fill, stroke) {
ctx.beginPath();
ctx.moveTo(...vertices[0]);
for (let i = 1; i < vertices.length; i++) {
ctx.lineTo(...vertices[i]);
}
if (close) {
ctx.closePath();
}
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
function drawRoundedRect(ctx, args, fill, stroke) {
const {
x,
y,
width,
height,
roundness
} = args;
ctx.beginPath();
ctx.moveTo(x + roundness, y);
ctx.arc(x + width - roundness, y + roundness, roundness, -0.5 * Math.PI, 0);
ctx.arc(x + width - roundness, y + height - roundness, roundness, 0, 0.5 * Math.PI);
ctx.arc(x + roundness, y + height - roundness, roundness, 0.5 * Math.PI, Math.PI);
ctx.arc(x + roundness, y + roundness, roundness, Math.PI, 1.5 * Math.PI);
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
function drawText(ctx, args, fill, stroke, options) {
const {
x,
y,
string
} = args;
ctx.font = options.fontSize + 'px ' + options.font;
ctx.textAlign = options.alignment;
ctx.textBaseline = options.baseline;
if (fill) {
ctx.fillText(string, x, y);
}
if (stroke) {
ctx.strokeText(string, x, y);
}
}
function drawTriangle(ctx, args, fill, stroke) {
const {
x1,
y1,
x2,
y2,
x3,
y3
} = args;
drawShape(ctx, true, [
[x1, y1],
[x2, y2],
[x3, y3]
], fill, stroke)
}

104
uno/client/socket.js Normal file
View File

@@ -0,0 +1,104 @@
export class SocketHandler {
constructor(url) {
this.url = url;
this.socket = new WebSocket(url);
this.socket.addEventListener('open', (e) => {
this.callbacks.open.forEach(cb => cb(e));
})
this.socket.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
this.callbacks.message.forEach((listener, index) => {
if (listener.type && listener.type !== data.type) {
return;
}
listener.cb(data.type, data.data, e);
if (listener.once) {
this.callbacks.message.splice(index, 1);
}
});
});
this.callbacks = {
open: [],
message: [],
close: [],
};
this.initialized = false;
this.gameToken = null;
}
async initGame() {
this.initialized = false;
this.gameToken = null;
return this.sendAndWaitForResult('game.init', {
playerCount: 4,
});
}
async joinGame(gameId) {
this.initialized = false;
this.gameToken = null;
return this.sendAndWaitForResult('game.join', {
gameID: gameId,
});
}
playCard(card) {
if (!this.initialized) {
return;
}
this.send('card.play', {
color: card.color,
value: card.value,
});
}
drawCard() {
if (!this.initialized) {
return;
}
this.send('card.draw', {})
}
selectColor(color) {
if (!this.initialized) {
return;
}
this.send('card.color', {
color
});
}
send(type, data) {
this.socket.send(JSON.stringify({
type,
data,
}));
}
async sendAndWaitForResult(type, data) {
return new Promise((resolve, reject) => {
this.send(type, data);
this.callbacks.message.push({
type: type + '.result',
once: true,
cb: (type, data) => {
resolve(data);
},
});
});
}
onOpen(callback) {
this.callbacks.open.push(callback);
}
onMessage(callback) {
this.callbacks.message.push(callback);
}
}

124
uno/client/styles.css Normal file
View File

@@ -0,0 +1,124 @@
* {
font-family: Roboto;
--color-red: #f92f2f;
--red-dark: #d01313;
--color-orange: #e07535;
--orange-dark: #bd5b20;
}
body {
margin: 0;
padding: 0;
overflow: hidden;
}
.dialog-container {
width: 100%;
height: 100%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
}
.dialog-container.hidden {
background: transparent;
pointer-events: none;
}
.dialog-container .container {
transition: transform 200ms, opacity 200ms;
}
.dialog-container.hidden .container {
transform: translateY(-100%);
opacity: 0;
pointer-events: none;
}
.container {
width: 250px;
display: flex;
flex-direction: column;
}
.dialog {
background: rgba(0, 0, 0, .75);
}
.dialog .container {
width: 400px;
}
.dialog-content {
width: 100%;
height: 100%;
border-radius: 10px;
background: #fff;
padding: 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.dialog-footer {
display: flex;
flex-direction: row-reverse;
}
.title-screen {
background: var(--color-red) radial-gradient(var(--color-orange), var(--color-red));
transition: background-color 200ms;
}
.btn {
padding: 16px 32px;
text-align: center;
background: var(--color-orange);
color: #ffffff;
border-radius: 10px;
border: 3px var(--orange-dark) solid;
outline: 0;
font-weight: bold;
font-size: 16px;
cursor: pointer;
margin-bottom: 16px;
transition: transform 200ms, box-shadow 200ms;
text-shadow: 3px
}
.btn:hover {
transform: scale(1.1);
}
.btn:active {
transform: scale(0.9);
}
input {
border: 3px rgba(0, 0, 0, .15) solid;
border-radius: 10px;
margin-bottom: 16px;
font-size: 16px;
padding: 8px 16px;
}
.primary {
background: var(--color-red);
color: #ffffff;
border-color: var(--red-dark);
}
.title {
font-size: 4em;
text-align: center;
color: #fff;
border: 5px #fff solid;
}
.margin-bottom {
margin-bottom: 32px;
}

137
uno/client/uno.js Normal file
View File

@@ -0,0 +1,137 @@
import {
Renderer
} from './render.js';
import {
EventHandler
} from './EventHandler.js';
import {
SocketHandler
} from './socket.js';
import {
Game
} from './Game.js';
import {
Card
} from './Card.js';
import {
Player
} from './Player.js';
const socketHandler = new SocketHandler('ws://localhost:8000');
socketHandler.onMessage({
cb: (type, data, e) => {
console.log(type, data, e);
}
});
export function triggerCreateGame() {
socketHandler.initGame().then(result => {
createGame(result);
});
}
export function triggerJoinGame(id) {
socketHandler.joinGame(id).then(result => {
createGame(result);
});
}
function createGame(data) {
socketHandler.initialized = true;
socketHandler.gameToken = data.gameID;
const game = new Game();
game.player.id = data.playerID;
game.activePlayerID = data.activePlayerID;
game.onCardPlayed = (card) => {
console.log(card);
socketHandler.playCard(card);
};
game.onCardDrawn = () => {
socketHandler.drawCard();
};
game.onColorSelected = (color) => {
socketHandler.selectColor(color);
};
if (data.players) {
data.players.forEach(player => game.playerJoined(new Player(game, player.playerID, player.playerName), player.cardCount));
}
game.init();
console.log(game);
game.player.setHand(data.hand.map(card => new Card(card.Color, card.Value, true)));
data.playingStack.forEach(card => {
game.playingStack.cards.unshift(new Card(card.Color, card.Value, true));
})
const canvas = document.getElementById('canvas');
const renderer = new Renderer(canvas, game);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.draw();
window.addEventListener('resize', () => {
console.log('test');
renderer.setSize(window.innerWidth, window.innerHeight);
});
const eventHandler = new EventHandler(canvas);
game.registerEventHandler(eventHandler);
socketHandler.onMessage({
type: 'game.joined',
cb: (type, data) => {
game.playerJoined(new Player(game, data.playerID, data.playerName), data.cardCount);
},
})
socketHandler.onMessage({
type: 'card.played',
cb: (type, data) => {
console.log(data);
game.playingStack.add(new Card(data.card.Color, data.card.Value, true));
},
});
socketHandler.onMessage({
type: 'card.color',
cb: (type, data) => {
console.log(data);
game.playingStack.setTopColor(data.color);
},
});
socketHandler.onMessage({
type: 'card.drawn',
cb: (type, data) => {
const player = game.players.find(p => p.id === data.playerID)
for (let i = 0; i < data.count; i++) {
player.addCard(new Card("", "", false));
}
},
});
socketHandler.onMessage({
type: 'color.choose',
cb: (type, data) => {
game.choosingColor = true;
},
});
socketHandler.onMessage({
type: 'turn.completed',
cb: (type, data) => {
game.choosingColor = false;
game.activePlayerID = data.activePlayerID;
game.clockwise = data.directionClockwise;
},
});
socketHandler.onMessage({
type: 'player.hand',
cb: (type, data) => {
console.log(data);
game.player.setHand(data.cards.map(card => new Card(card.Color, card.Value, true)));
},
})
}