Initial commit
This commit is contained in:
parent
3c4ec2f6dc
commit
9922e5470c
22
uno/.vscode/launch.json
vendored
Normal file
22
uno/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [{
|
||||
"name": "Launch Go",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "remote",
|
||||
"remotePath": "",
|
||||
"port": 2345,
|
||||
"host": "127.0.0.1",
|
||||
"program": "${workspaceFolder}/server",
|
||||
"showLog": true
|
||||
},
|
||||
{
|
||||
"name": "Launch index.html",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"file": "${workspaceFolder}/client/index.html"
|
||||
},
|
||||
]
|
||||
}
|
3
uno/.vscode/settings.json
vendored
Normal file
3
uno/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"liveServer.settings.multiRootWorkspaceName": "uno"
|
||||
}
|
42
uno/client/Card.js
Normal file
42
uno/client/Card.js
Normal 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
30
uno/client/CardDeck.js
Normal 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);
|
66
uno/client/EventHandler.js
Normal file
66
uno/client/EventHandler.js
Normal 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
206
uno/client/Game.js
Normal 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
90
uno/client/Player.js
Normal 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
31
uno/client/Stack.js
Normal 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
18
uno/client/helpers.js
Normal 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
39
uno/client/index.html
Normal 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
36
uno/client/menu.js
Normal 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
574
uno/client/render.js
Normal 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
104
uno/client/socket.js
Normal 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
124
uno/client/styles.css
Normal 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
137
uno/client/uno.js
Normal 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)));
|
||||
},
|
||||
})
|
||||
}
|
BIN
uno/server/__debug_bin
Normal file
BIN
uno/server/__debug_bin
Normal file
Binary file not shown.
8
uno/server/go.mod
Normal file
8
uno/server/go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module kingofdog.de/projects/uno
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
)
|
4
uno/server/go.sum
Normal file
4
uno/server/go.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
87
uno/server/helpers.go
Normal file
87
uno/server/helpers.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetDefaultDeck() []Card {
|
||||
cards := make([]Card, 0)
|
||||
for _, color := range []string{"GREEN", "BLUE", "RED", "YELLOW"} {
|
||||
for i := 0; i < 10; i++ {
|
||||
cards = append(cards, Card{
|
||||
color, strconv.Itoa(i),
|
||||
})
|
||||
}
|
||||
cards = append(cards, Card{
|
||||
color, "RETURN",
|
||||
})
|
||||
cards = append(cards, Card{
|
||||
color, "BLOCK",
|
||||
})
|
||||
cards = append(cards, Card{
|
||||
color, "DRAW2",
|
||||
})
|
||||
}
|
||||
cards = append(cards, Card{
|
||||
"BLACK", "CHOOSE",
|
||||
})
|
||||
cards = append(cards, Card{
|
||||
"BLACK", "DRAW4",
|
||||
})
|
||||
return cards
|
||||
}
|
||||
|
||||
func GetGame(id string) *Game {
|
||||
return games[id]
|
||||
}
|
||||
|
||||
func GetRandomCard(cards []Card) *Card {
|
||||
index := rand.Intn(len(cards))
|
||||
return &cards[index]
|
||||
}
|
||||
|
||||
func FindCard(cards []Card, color string, value string) (*Card, bool) {
|
||||
for _, card := range cards {
|
||||
if card.Color == color && card.Value == value {
|
||||
return &card, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func FindOnHand(player *Player, card *Card) int {
|
||||
for index, handCard := range player.Hand {
|
||||
if handCard.Color == card.Color && handCard.Value == card.Value {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func RemoveFromHand(player *Player, index int) []*Card {
|
||||
player.Hand[len(player.Hand)-1], player.Hand[index] = player.Hand[index], player.Hand[len(player.Hand)-1]
|
||||
return player.Hand[:len(player.Hand)-1]
|
||||
}
|
||||
|
||||
func InitPlayer(game *Game, player *Player) {
|
||||
player.Game = game
|
||||
for i := 0; i < 10; i++ {
|
||||
player.Hand = append(player.Hand, GetRandomCard(game.AvailableCards))
|
||||
}
|
||||
}
|
||||
|
||||
func DrawNextPlayer(game *Game, count int) {
|
||||
nextPlayer := game.GetNextPlayer()
|
||||
for i := 0; i < count; i++ {
|
||||
nextPlayer.Hand = append(nextPlayer.Hand, GetRandomCard(game.AvailableCards))
|
||||
}
|
||||
|
||||
game.send("card.drawn", map[string]interface{}{
|
||||
"playerID": nextPlayer.ID,
|
||||
"count": count,
|
||||
})
|
||||
nextPlayer.send("player.hand", map[string]interface{}{
|
||||
"cards": nextPlayer.Hand,
|
||||
})
|
||||
}
|
352
uno/server/main.go
Normal file
352
uno/server/main.go
Normal file
|
@ -0,0 +1,352 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var games = make(map[string]*Game, 0)
|
||||
var players = make(map[string]*Player, 0)
|
||||
|
||||
var addr = flag.String("addr", ":8000", "http service address")
|
||||
|
||||
func (p *Player) send(messageType string, data map[string]interface{}) {
|
||||
dataObject := DataObject{
|
||||
Type: messageType,
|
||||
Data: data,
|
||||
}
|
||||
p.Connection.WriteJSON(dataObject)
|
||||
}
|
||||
|
||||
func (g *Game) send(messageType string, data map[string]interface{}) {
|
||||
for _, player := range g.Players {
|
||||
log.Println("sending message", player)
|
||||
player.send(messageType, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) GetNextPlayer() *Player {
|
||||
index := g.CurrentPlayerIndex
|
||||
if g.DirectionClockwise {
|
||||
index++
|
||||
} else {
|
||||
index--
|
||||
}
|
||||
|
||||
if index < 0 {
|
||||
index += len(g.Players)
|
||||
} else if index >= len(g.Players) {
|
||||
index -= len(g.Players)
|
||||
}
|
||||
|
||||
return g.Players[index]
|
||||
}
|
||||
|
||||
func (g *Game) nextTurn(block bool) {
|
||||
count := 1
|
||||
if block {
|
||||
count++
|
||||
}
|
||||
if g.DirectionClockwise {
|
||||
g.CurrentPlayerIndex += count
|
||||
} else {
|
||||
g.CurrentPlayerIndex -= count
|
||||
}
|
||||
|
||||
if g.CurrentPlayerIndex < 0 {
|
||||
g.CurrentPlayerIndex += len(g.Players)
|
||||
} else if g.CurrentPlayerIndex >= len(g.Players) {
|
||||
g.CurrentPlayerIndex -= len(g.Players)
|
||||
}
|
||||
|
||||
g.send("game.nextTurn", map[string]interface{}{
|
||||
"playerID": g.Players[g.CurrentPlayerIndex].ID,
|
||||
})
|
||||
}
|
||||
|
||||
func gameInitHandler(player *Player, data map[string]interface{}) {
|
||||
game := Game{
|
||||
//ID: uuid.New().String(),
|
||||
ID: "1234",
|
||||
MaxPlayers: 4,
|
||||
DirectionClockwise: true,
|
||||
AvailableCards: GetDefaultDeck(),
|
||||
}
|
||||
game.Players = append(game.Players, player)
|
||||
game.PlayingStack.Cards = append(game.PlayingStack.Cards, GetRandomCard(game.AvailableCards))
|
||||
game.CurrentPlayerIndex = 0
|
||||
games[game.ID] = &game
|
||||
InitPlayer(&game, player)
|
||||
|
||||
player.send("game.init.result", map[string]interface{}{
|
||||
"gameID": game.ID,
|
||||
"hand": player.Hand,
|
||||
"playerID": player.ID,
|
||||
"activePlayerID": game.Players[game.CurrentPlayerIndex].ID,
|
||||
"playingStack": game.PlayingStack.Cards,
|
||||
})
|
||||
}
|
||||
|
||||
func joinGameHandler(player *Player, data map[string]interface{}) {
|
||||
gameID, ok := data["gameID"]
|
||||
if !ok {
|
||||
log.Println("not found game id")
|
||||
return
|
||||
}
|
||||
|
||||
game, ok := games[gameID.(string)]
|
||||
if !ok {
|
||||
log.Println("game not found")
|
||||
return
|
||||
}
|
||||
|
||||
InitPlayer(game, player)
|
||||
game.send("game.joined", map[string]interface{}{
|
||||
"playerID": player.ID,
|
||||
"playerName": player.Name,
|
||||
"cardCount": len(player.Hand),
|
||||
})
|
||||
|
||||
players := make([]struct {
|
||||
PlayerID string `json:"playerID"`
|
||||
PlayerName string `json:"playerName"`
|
||||
CardCount int `json:"cardCount"`
|
||||
}, len(game.Players))
|
||||
for index, p := range game.Players {
|
||||
players[index] = struct {
|
||||
PlayerID string `json:"playerID"`
|
||||
PlayerName string `json:"playerName"`
|
||||
CardCount int `json:"cardCount"`
|
||||
}{
|
||||
PlayerID: p.ID,
|
||||
PlayerName: p.Name,
|
||||
CardCount: len(p.Hand),
|
||||
}
|
||||
}
|
||||
|
||||
game.Players = append(game.Players, player)
|
||||
|
||||
log.Println(game)
|
||||
player.send("game.join.result", map[string]interface{}{
|
||||
"hand": player.Hand,
|
||||
"playerID": player.ID,
|
||||
"activePlayerID": game.Players[game.CurrentPlayerIndex].ID,
|
||||
"players": players,
|
||||
"playingStack": game.PlayingStack.Cards,
|
||||
})
|
||||
}
|
||||
|
||||
func playCardHandler(player *Player, data map[string]interface{}) {
|
||||
log.Println("card")
|
||||
if player.Game == nil {
|
||||
return
|
||||
}
|
||||
log.Println("color")
|
||||
|
||||
if player.Game.Players[player.Game.CurrentPlayerIndex].ID != player.ID {
|
||||
return
|
||||
}
|
||||
|
||||
if player.Game.ChoosingColor {
|
||||
return
|
||||
}
|
||||
|
||||
color, ok := data["color"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
value, ok := data["value"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
card, ok := FindCard(player.Game.AvailableCards, color.(string), value.(string))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
handIndex := FindOnHand(player, card)
|
||||
if handIndex == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
topCard := player.Game.PlayingStack.Cards[len(player.Game.PlayingStack.Cards)-1]
|
||||
log.Println(topCard)
|
||||
if card.Color != "BLACK" && topCard.Value != card.Value && topCard.Color != card.Color {
|
||||
return
|
||||
}
|
||||
|
||||
player.Game.PlayingStack.Cards = append(player.Game.PlayingStack.Cards, card)
|
||||
player.Hand = RemoveFromHand(player, handIndex)
|
||||
|
||||
if card.Value == "RETURN" {
|
||||
player.Game.DirectionClockwise = !player.Game.DirectionClockwise
|
||||
}
|
||||
if card.Value == "DRAW2" {
|
||||
DrawNextPlayer(player.Game, 2)
|
||||
}
|
||||
if card.Value == "DRAW4" {
|
||||
DrawNextPlayer(player.Game, 4)
|
||||
}
|
||||
|
||||
chooseColor := card.Value == "CHOOSE" || card.Value == "DRAW4"
|
||||
|
||||
if !chooseColor {
|
||||
block := card.Value == "BLOCK" || card.Value == "DRAW2"
|
||||
player.Game.nextTurn(block)
|
||||
}
|
||||
|
||||
log.Println(card)
|
||||
player.Game.send("card.played", map[string]interface{}{
|
||||
"playerID": player.ID,
|
||||
"card": &card,
|
||||
})
|
||||
|
||||
player.send("player.hand", map[string]interface{}{
|
||||
"cards": player.Hand,
|
||||
})
|
||||
|
||||
if chooseColor {
|
||||
player.Game.ChoosingColor = true
|
||||
player.send("color.choose", map[string]interface{}{})
|
||||
}
|
||||
|
||||
if !chooseColor {
|
||||
player.Game.send("turn.completed", map[string]interface{}{
|
||||
"activePlayerID": player.Game.Players[player.Game.CurrentPlayerIndex].ID,
|
||||
"directionClockwise": player.Game.DirectionClockwise,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func chooseColorHandler(player *Player, data map[string]interface{}) {
|
||||
if player.Game == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if player.Game.Players[player.Game.CurrentPlayerIndex].ID != player.ID {
|
||||
return
|
||||
}
|
||||
|
||||
if !player.Game.ChoosingColor {
|
||||
return
|
||||
}
|
||||
|
||||
color := data["color"]
|
||||
if color != "GREEN" && color != "BLUE" && color != "RED" && color != "YELLOW" {
|
||||
return
|
||||
}
|
||||
|
||||
topCard := player.Game.PlayingStack.Cards[len(player.Game.PlayingStack.Cards)-1]
|
||||
topCard.Color = color.(string)
|
||||
|
||||
player.Game.ChoosingColor = false
|
||||
player.Game.send("card.color", map[string]interface{}{
|
||||
"color": color,
|
||||
})
|
||||
|
||||
player.Game.nextTurn(topCard.Value == "DRAW4")
|
||||
|
||||
player.Game.send("turn.completed", map[string]interface{}{
|
||||
"activePlayerID": player.Game.Players[player.Game.CurrentPlayerIndex].ID,
|
||||
"directionClockwise": player.Game.DirectionClockwise,
|
||||
})
|
||||
}
|
||||
|
||||
func drawCardHandler(player *Player, data map[string]interface{}) {
|
||||
if player.Game == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if player.Game.Players[player.Game.CurrentPlayerIndex].ID != player.ID {
|
||||
return
|
||||
}
|
||||
|
||||
player.Hand = append(player.Hand, GetRandomCard(player.Game.AvailableCards))
|
||||
player.Game.nextTurn(false)
|
||||
log.Println(player.Hand)
|
||||
|
||||
player.Game.send("card.drawn", map[string]interface{}{
|
||||
"playerID": player.ID,
|
||||
"count": 1,
|
||||
})
|
||||
|
||||
player.send("player.hand", map[string]interface{}{
|
||||
"cards": player.Hand,
|
||||
})
|
||||
|
||||
player.Game.send("turn.completed", map[string]interface{}{
|
||||
"activePlayerID": player.Game.Players[player.Game.CurrentPlayerIndex].ID,
|
||||
"directionClockwise": player.Game.DirectionClockwise,
|
||||
})
|
||||
}
|
||||
|
||||
func playerLogoutHandler(player *Player) {
|
||||
delete(players, player.ID)
|
||||
}
|
||||
|
||||
func ws(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
player := Player{
|
||||
ID: uuid.New().String(),
|
||||
Name: "Test Player",
|
||||
Connection: conn,
|
||||
}
|
||||
players[player.ID] = &player
|
||||
|
||||
for {
|
||||
var data DataObject
|
||||
err := conn.ReadJSON(&data)
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, 1001) {
|
||||
log.Println("closed connection")
|
||||
playerLogoutHandler(&player)
|
||||
}
|
||||
|
||||
log.Printf("Failed to read message %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
log.Println(data)
|
||||
|
||||
switch data.Type {
|
||||
case "game.init":
|
||||
gameInitHandler(&player, data.Data)
|
||||
log.Println("Initializing game")
|
||||
case "game.join":
|
||||
joinGameHandler(&player, data.Data)
|
||||
case "card.play":
|
||||
playCardHandler(&player, data.Data)
|
||||
case "card.color":
|
||||
chooseColorHandler(&player, data.Data)
|
||||
case "card.draw":
|
||||
drawCardHandler(&player, data.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
http.HandleFunc("/", ws)
|
||||
|
||||
log.Println("listening")
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("listen and serve", err)
|
||||
}
|
||||
}
|
46
uno/server/models.go
Normal file
46
uno/server/models.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// DataObject is used for parsing and encoding the communication with WebSocket clients
|
||||
type DataObject struct {
|
||||
Type string `json:"type"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// Game is a model for a game instance of UNO which can be joined by users
|
||||
type Game struct {
|
||||
ID string
|
||||
MaxPlayers int
|
||||
Players []*Player
|
||||
|
||||
DirectionClockwise bool
|
||||
CurrentPlayerIndex int
|
||||
ChoosingColor bool
|
||||
|
||||
DrawingStack Stack
|
||||
PlayingStack Stack
|
||||
|
||||
AvailableCards []Card
|
||||
}
|
||||
|
||||
// Player contains data about a connected player
|
||||
type Player struct {
|
||||
ID string
|
||||
Name string
|
||||
Connection *websocket.Conn
|
||||
Game *Game
|
||||
|
||||
Hand []*Card
|
||||
}
|
||||
|
||||
type Stack struct {
|
||||
Cards []*Card
|
||||
}
|
||||
|
||||
type Card struct {
|
||||
Color string
|
||||
Value string
|
||||
}
|
Reference in New Issue
Block a user