Uno

28 May 2021 — Written by Jorge Abundis
#React#Socket.io

Virtual rendition of the traditional Uno game as an online, real-time, multiplayer party game.

This app is the process of being built. The mechanics of the game are complete. The remaining parts of the rendition is the design of the style.

This app was built using React, Node.js, and Socket.IO

Server

Players will be able to join a room with a specific code that can be shared to other players. Server Helper functions to keep track of the users in the given room:

const users = [];

const addUser = ({ id, name, room }) => {
  const usersInRoom = users.filter((user) => user.room === room).length;
  if (usersInRoom === 4) {
    return { error: "Can't Join, Room is full" };
  }

  const newUser = { id, name, room };
  users.push(newUser);
  return { newUser };
};

const removeUser = (id) => {
  const findUserIndex = users.findIndex((user) => user.id === id);

  if (findUserIndex !== -1) {
    return users.splice(findUserIndex, 1)[0];
  }
};

const getUser = (id) => {
  return users.find((user) => user.id === id);
};

const getUsersInCertainRoom = (room) => {
  return users.filter((user) => user.room == room);
};

module.exports = { addUser, removeUser, getUser, getUsersInCertainRoom };

This is the Node.js index file in which we use Socket.IO to control the room data. We have socket functions to initialize and update the uno game state in our front end.

const express = require("express");
const socketio = require("socket.io");
const http = require("http");
const cors = require("cors");

const {
  addUser,
  removeUser,
  getUser,
  getUsersInCertainRoom,
} = require("./users");

const PORT = process.env.PORT || 3001;

const app = express();
const server = http.createServer(app);
const io = socketio(server);

app.use(cors());

io.on("connection", (socket) => {
  console.log("ROOM");

  socket.on("join_room", (data, callback) => {
    let numUsersinRoom = getUsersInCertainRoom(data.room).length;

    let names;
    if (numUsersinRoom === 0) {
      names = "Player 1";
    } else if (numUsersinRoom === 1) {
      names = "Player 2";
    } else if (numUsersinRoom === 2) {
      names = "Player 3";
    } else {
      names = "Player 4";
    }

    const { error, newUser } = addUser({
      id: socket.id,
      name: names,
      room: data.room,
    });

    if (error) {
      return callback(error);
    }

    socket.join(newUser.room);


    io.to(newUser.room).emit("roomData", {
      room: newUser.room,
      users: getUsersInCertainRoom(newUser.room),
    });
    socket.emit("currentUserData", { name: newUser.name });
    callback();
  });

  socket.on("initGameState", (gameState) => {
    const user = getUser(socket.id);
    if (user) io.to(user.room).emit("initGameState", gameState);
  });

  socket.on("updateGameState", (gameState) => {
    const user = getUser(socket.id);
    if (user) io.to(user.room).emit("updateGameState", gameState);
  });

  socket.on("sendMessage", (data, callback) => {
    const user = getUser(socket.id);
    io.to(user.room).emit("message", { user: user.name, text: data.message });
    callback();
  });

  socket.on("disconnect", () => {
    const user = removeUser(socket.id);
    io.to(user.room).emit("roomData", {
      room: user.room,
      users: getUsersInCertainRoom(user.room),
    });
    console.log("USER DISCONNECTED");
  });
});

server.listen(PORT, () => {
  console.log(`Server Running on ${PORT}`);
});

In the front end, there is a useEffect that initializes and updates the game state for every player

 useEffect(() => {
    const deck = buildDeck();
    const player1_deck = deck.splice(0, 7);
    const player2_deck = deck.splice(0, 7);
    const player3_deck = deck.splice(0, 7);
    const player4_deck = deck.splice(0, 7);

    let cardIndex;
    while (true) {
      cardIndex = Math.floor(Math.random() * 100);
      if (
        cards[deck[cardIndex]].value === "WILD" ||
        cards[deck[cardIndex]].value === "WILDPLUS" ||
        cards[deck[cardIndex]].value === "REVERSE" ||
        cards[deck[cardIndex]].value === "SKIP" ||
        cards[deck[cardIndex]].value === "PLUS"
      ) {
        continue;
      } else break;
    }
    const graveyard = deck.splice(cardIndex, 1);
    const currentCard = {
      color: cards[graveyard[0]]?.color,
      value: cards[graveyard[0]]?.value,
    };

    socket.emit("initGameState", {
      gameOver: false,
      turn: "Player 1",
      player1_deck: [...player1_deck],
      player2_deck: [...player2_deck],
      player3_deck: [...player3_deck],
      player4_deck: [...player4_deck],
      currentCard: currentCard,
      graveyard: [...graveyard],
      deck: [...deck],
      player1Turn: true,
      player2Turn: false,
      player3Turn: false,
      player4Turn: false,
    });
  }, []);

  useEffect(() => {
    socket.on(
      "initGameState",
      ({
        gameOver,
        turn,
        player1_deck,
        player2_deck,
        player3_deck,
        player4_deck,
        currentCard,
        graveyard,
        deck,
        player1Turn,
        player2Turn,
        player3Turn,
        player4Turn,
      }) => {
        setGameOver(gameOver);
        setTurn(turn);
        setplayer1_deck(player1_deck);
        setplayer2_deck(player2_deck);
        setplayer3_deck(player3_deck);
        setplayer4_deck(player4_deck);
        setCurrentCard(currentCard);
        setgraveyard(graveyard);
        setDeck(deck);
        setplayer1Turn(player1Turn);
        setplayer2Turn(player2Turn);
        setplayer3Turn(player3Turn);
        setplayer4Turn(player4Turn);
      }
    );

    socket.on(
      "updateGameState",
      ({
        gameOver,
        turn,
        player1_deck,
        player2_deck,
        player3_deck,
        player4_deck,
        currentCard,
        graveyard,
        deck,
        player1Turn,
        player2Turn,
        player3Turn,
        player4Turn,
      }) => {
        gameOver && setGameOver(gameOver);
        turn && setTurn(turn);
        player1_deck && setplayer1_deck(player1_deck);
        player2_deck && setplayer2_deck(player2_deck);
        player3_deck && setplayer3_deck(player3_deck);
        player4_deck && setplayer4_deck(player4_deck);
        currentCard && setCurrentCard(currentCard);
        graveyard && setgraveyard(graveyard);
        deck && setDeck(deck);
        player1Turn && setplayer1Turn(player1Turn);
        player2Turn && setplayer2Turn(player2Turn);
        player3Turn && setplayer3Turn(player3Turn);
        player4Turn && setplayer4Turn(player4Turn);
      }
    );

    socket.on("roomData", ({ users }) => {
      setUsers(users);
    });

    socket.on("currentUserData", ({ name }) => {
      setCurrentUser(name);
    });

    socket.on("message", (message) => {
      setMessages((messages) => [...messages, message]);
    });
  }, []);

There are 4 functions that take care of the logic based on each players move. The player moves are sent to the room and the game state is updated. This will update the game state for all of the players.

const player1_playedCard = (index) => {
    if (cards[player1_deck[index]].value == "WILDPLUS") {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setShowPie(true);
      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer2_deck(player2_deck.concat(deck.splice(0, 4)));
      } else {
        setplayer4_deck(player4_deck.concat(deck.splice(0, 4)));
      }

      setplayer3Turn(true);
      setplayer1Turn(false);
      setplayer2Turn(false);
      setplayer4Turn(false);

      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 3",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (cards[player1_deck[index]].value == "WILD") {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setShowPie(true);
      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer2Turn(true);
        setplayer1Turn(false);
        setplayer3Turn(false);
        setplayer4Turn(false);
        setTurn("Player Two");
      } else {
        setplayer4Turn(true);
        setplayer1Turn(false);
        setplayer2Turn(false);
        setplayer4Turn(false);
        setTurn("Player Four");
      }
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (
      cards[player1_deck[index]].color === currentCard.color &&
      cards[player1_deck[index]].value === "PLUS"
    ) {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer2_deck(player2_deck.concat(deck.splice(0, 2)));
      } else {
        setplayer4_deck(player4_deck.concat(deck.splice(0, 2)));
      }
      setplayer3Turn(true);
      setplayer1Turn(false);
      setplayer2Turn(false);
      setplayer4Turn(false);
      setTurn("Player Three");
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (
      cards[player1_deck[index]].color === currentCard.color &&
      cards[player1_deck[index]].value === "REVERSE"
    ) {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer4Turn(true);
        setplayer1Turn(false);
        setplayer2Turn(false);
        setplayer3Turn(false);
        setTurn("Player Four");
      } else {
        setplayer2Turn(true);
        setplayer1Turn(false);
        setplayer3Turn(false);
        setplayer4Turn(false);
        setTurn("Player Two");
      }
      setdirection(!direction);
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (
      cards[player1_deck[index]].color === currentCard.color &&
      cards[player1_deck[index]].value === "SKIP"
    ) {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      setplayer3Turn(true);
      setplayer1Turn(false);
      setplayer2Turn(false);
      setplayer4Turn(false);
      setTurn("Player Three");
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (
      cards[player1_deck[index]].value == currentCard.value &&
      cards[player1_deck[index]].value == "PLUS"
    ) {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer2_deck(player2_deck.concat(deck.splice(0, 2)));
      } else {
        setplayer4_deck(player4_deck.concat(deck.splice(0, 2)));
      }
      setplayer3Turn(true);
      setplayer1Turn(false);
      setplayer2Turn(false);
      setplayer4Turn(false);
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (
      cards[player1_deck[index]].value == currentCard.value &&
      cards[player1_deck[index]].value == "REVERSE"
    ) {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer4Turn(true);
        setplayer1Turn(false);
        setplayer2Turn(false);
        setplayer3Turn(false);
      } else {
        setplayer2Turn(true);
        setplayer1Turn(false);
        setplayer3Turn(false);
        setplayer4Turn(false);
      }
      setdirection(!direction);
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (
      cards[player1_deck[index]].value == currentCard.value &&
      cards[player1_deck[index]].value == "SKIP"
    ) {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      setplayer3Turn(true);
      setplayer1Turn(false);
      setplayer2Turn(false);
      setplayer4Turn(false);
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (cards[player1_deck[index]].value == currentCard.value) {
      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer2Turn(true);
        setplayer1Turn(false);
        setplayer3Turn(false);
        setplayer4Turn(false);
      } else {
        setplayer4Turn(true);
        setplayer1Turn(false);
        setplayer2Turn(false);
        setplayer3Turn(false);
      }
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else if (cards[player1_deck[index]].color == currentCard.color) {

      const currentCard = {
        color: cards[player1_deck[index]].color,
        value: cards[player1_deck[index]].value,
      };
      const graveyard1 = graveyard.concat(player1_deck.splice(index, 1));

      setplayer1_deck(player1_deck);
      if (direction === true) {
        setplayer2Turn(true);
        setplayer1Turn(false);
        setplayer3Turn(false);
        setplayer4Turn(false);
      } else {
        setplayer4Turn(true);
        setplayer1Turn(false);
        setplayer2Turn(false);
        setplayer3Turn(false);
      }
      socket.emit("updateGameState", {
        gameOver: checkGameOver(player1_deck),
        turn: "Player 4",
        player1_deck: [...player1_deck],
        player2_deck: [...player2_deck],
        player3_deck: [...player3_deck],
        player4_deck: [...player4_deck],
        currentCard: currentCard,
        graveyard: [...graveyard1],
        deck: [...deck],
        player1Turn: player1Turn,
        player2Turn: player2Turn,
        player3Turn: player3Turn,
        player4Turn: player4Turn,
      });
    } else {
      alert("Play a valid card or draw a card");
    }

  };

Each player will have their own player view. Their own persepctive decks will be at the bottom of the page. This is done through checking what player number they are.

currentUser === "Player 1"