diff --git a/uno/.vscode/launch.json b/uno/.vscode/launch.json
new file mode 100644
index 0000000..8aca536
--- /dev/null
+++ b/uno/.vscode/launch.json
@@ -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"
+ },
+ ]
+}
\ No newline at end of file
diff --git a/uno/.vscode/settings.json b/uno/.vscode/settings.json
new file mode 100644
index 0000000..05cb3bf
--- /dev/null
+++ b/uno/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "liveServer.settings.multiRootWorkspaceName": "uno"
+}
\ No newline at end of file
diff --git a/uno/client/Card.js b/uno/client/Card.js
new file mode 100644
index 0000000..f66a9a0
--- /dev/null
+++ b/uno/client/Card.js
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/uno/client/CardDeck.js b/uno/client/CardDeck.js
new file mode 100644
index 0000000..5951552
--- /dev/null
+++ b/uno/client/CardDeck.js
@@ -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);
\ No newline at end of file
diff --git a/uno/client/EventHandler.js b/uno/client/EventHandler.js
new file mode 100644
index 0000000..a331397
--- /dev/null
+++ b/uno/client/EventHandler.js
@@ -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,
+ });
+ }
+}
\ No newline at end of file
diff --git a/uno/client/Game.js b/uno/client/Game.js
new file mode 100644
index 0000000..a0ed0e4
--- /dev/null
+++ b/uno/client/Game.js
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/uno/client/Player.js b/uno/client/Player.js
new file mode 100644
index 0000000..796f3e3
--- /dev/null
+++ b/uno/client/Player.js
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/uno/client/Stack.js b/uno/client/Stack.js
new file mode 100644
index 0000000..64aa600
--- /dev/null
+++ b/uno/client/Stack.js
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/uno/client/helpers.js b/uno/client/helpers.js
new file mode 100644
index 0000000..bae6290
--- /dev/null
+++ b/uno/client/helpers.js
@@ -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,
+ };
+}
\ No newline at end of file
diff --git a/uno/client/index.html b/uno/client/index.html
new file mode 100644
index 0000000..97196da
--- /dev/null
+++ b/uno/client/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ UNO
+
+
+
+
+
+
+
ETT.IO
+
+
+
+
+
+
+
+
+
+
Enter a game code:
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/uno/client/menu.js b/uno/client/menu.js
new file mode 100644
index 0000000..c972a1e
--- /dev/null
+++ b/uno/client/menu.js
@@ -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');
+}
\ No newline at end of file
diff --git a/uno/client/render.js b/uno/client/render.js
new file mode 100644
index 0000000..140ac7a
--- /dev/null
+++ b/uno/client/render.js
@@ -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)
+}
\ No newline at end of file
diff --git a/uno/client/socket.js b/uno/client/socket.js
new file mode 100644
index 0000000..a8130a8
--- /dev/null
+++ b/uno/client/socket.js
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/uno/client/styles.css b/uno/client/styles.css
new file mode 100644
index 0000000..7bcb38d
--- /dev/null
+++ b/uno/client/styles.css
@@ -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;
+}
\ No newline at end of file
diff --git a/uno/client/uno.js b/uno/client/uno.js
new file mode 100644
index 0000000..6a39bcd
--- /dev/null
+++ b/uno/client/uno.js
@@ -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)));
+ },
+ })
+}
\ No newline at end of file
diff --git a/uno/server/__debug_bin b/uno/server/__debug_bin
new file mode 100644
index 0000000..8457719
Binary files /dev/null and b/uno/server/__debug_bin differ
diff --git a/uno/server/go.mod b/uno/server/go.mod
new file mode 100644
index 0000000..5359ce6
--- /dev/null
+++ b/uno/server/go.mod
@@ -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
+)
diff --git a/uno/server/go.sum b/uno/server/go.sum
new file mode 100644
index 0000000..d8bf7d2
--- /dev/null
+++ b/uno/server/go.sum
@@ -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=
diff --git a/uno/server/helpers.go b/uno/server/helpers.go
new file mode 100644
index 0000000..e497a4b
--- /dev/null
+++ b/uno/server/helpers.go
@@ -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,
+ })
+}
diff --git a/uno/server/main.go b/uno/server/main.go
new file mode 100644
index 0000000..1246129
--- /dev/null
+++ b/uno/server/main.go
@@ -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)
+ }
+}
diff --git a/uno/server/models.go b/uno/server/models.go
new file mode 100644
index 0000000..1f09df0
--- /dev/null
+++ b/uno/server/models.go
@@ -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
+}