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