├── .husky
├── .gitignore
├── pre-push
└── pre-commit
├── .prettierrc.json
├── public
├── cards
│ ├── 00.png
│ ├── 01.png
│ ├── 02.png
│ ├── 03.png
│ ├── 04.png
│ ├── 05.png
│ ├── 06.png
│ ├── 07.png
│ ├── 08.png
│ ├── 09.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.png
│ ├── m1.png
│ ├── m2.png
│ ├── back.png
│ └── blank.png
├── favicon.ico
├── fonts
│ ├── Roboto-Black.ttf
│ ├── Roboto-Bold.ttf
│ ├── Roboto-Light.ttf
│ ├── Roboto-Medium.ttf
│ ├── Roboto-Thin.ttf
│ └── Roboto-Regular.ttf
├── arrow.svg
├── arrow_right.svg
├── players.svg
└── vercel.svg
├── next-env.d.ts
├── .stylelintrc.json
├── nodemon.json
├── .lintstagedrc.json
├── .storybook
├── preview.js
└── main.js
├── components
├── misc
│ ├── PlayerCount.module.css
│ ├── LobbyListItem.module.css
│ ├── PlayerCount.tsx
│ ├── Misc.stories.tsx
│ └── LobbyListItem.tsx
├── button
│ ├── RulesBtn.tsx
│ ├── StartGameBtn.tsx
│ ├── ConfirmBtn.tsx
│ ├── BackBtn.tsx
│ ├── types.ts
│ ├── ExitBtn.tsx
│ ├── Continue.tsx
│ ├── CreateBtn.tsx
│ ├── KeepBtn.tsx
│ ├── DiscardBtn.tsx
│ ├── ReadyBtn.tsx
│ ├── RestartBtn.tsx
│ ├── JoinBtn.tsx
│ ├── Button.stories.tsx
│ └── Button.module.css
├── logo
│ ├── Logo.tsx
│ ├── Logo.stories.tsx
│ └── Logo.module.css
├── gameboard
│ ├── DrawPilePrompt.module.css
│ ├── types.ts
│ ├── Piles.module.css
│ ├── RoundCounter.tsx
│ ├── OpponentCardGrid.tsx
│ ├── Score.module.css
│ ├── RoundScoreModal.tsx
│ ├── Statusbar.tsx
│ ├── DiscardPile.tsx
│ ├── TotalScore.tsx
│ ├── DrawPile.tsx
│ ├── MiscElements.module.css
│ ├── GameBoard.stories.tsx
│ ├── CardGrid.module.css
│ ├── DrawPilePrompt.tsx
│ └── CardGrid.tsx
├── input
│ ├── Input.stories.tsx
│ ├── NameInput.tsx
│ └── Input.module.css
└── pages
│ └── Pages.stories.tsx
├── .eslintignore
├── .prettierignore
├── tsconfig.server.json
├── lib
└── functions.ts
├── .github
├── ISSUE_TEMPLATE
│ └── issue--user-story-.md
└── workflows
│ └── node.js.yml
├── styles
├── Home.module.css
├── User.module.css
├── Rules.module.css
├── Lobbies.module.css
├── Game.module.css
└── globals.css
├── pages
├── _app.tsx
├── index.tsx
├── user.tsx
├── rules.tsx
├── lobbies.tsx
└── game.tsx
├── server
├── lib
│ ├── turnPhases.ts
│ ├── statusMessages.ts
│ ├── gameTypes.ts
│ ├── cards.ts
│ ├── broadcasts.ts
│ └── games.ts
├── index.ts
└── socket.ts
├── tsconfig.json
├── .gitignore
├── .eslintrc.json
├── contexts
└── SocketContext.tsx
├── package.json
└── README.md
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm test
5 |
--------------------------------------------------------------------------------
/public/cards/00.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/00.png
--------------------------------------------------------------------------------
/public/cards/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/01.png
--------------------------------------------------------------------------------
/public/cards/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/02.png
--------------------------------------------------------------------------------
/public/cards/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/03.png
--------------------------------------------------------------------------------
/public/cards/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/04.png
--------------------------------------------------------------------------------
/public/cards/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/05.png
--------------------------------------------------------------------------------
/public/cards/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/06.png
--------------------------------------------------------------------------------
/public/cards/07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/07.png
--------------------------------------------------------------------------------
/public/cards/08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/08.png
--------------------------------------------------------------------------------
/public/cards/09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/09.png
--------------------------------------------------------------------------------
/public/cards/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/10.png
--------------------------------------------------------------------------------
/public/cards/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/11.png
--------------------------------------------------------------------------------
/public/cards/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/12.png
--------------------------------------------------------------------------------
/public/cards/m1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/m1.png
--------------------------------------------------------------------------------
/public/cards/m2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/m2.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/cards/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/back.png
--------------------------------------------------------------------------------
/public/cards/blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/cards/blank.png
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/public/fonts/Roboto-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/fonts/Roboto-Black.ttf
--------------------------------------------------------------------------------
/public/fonts/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/fonts/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/fonts/Roboto-Light.ttf
--------------------------------------------------------------------------------
/public/fonts/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/fonts/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/public/fonts/Roboto-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/fonts/Roboto-Thin.ttf
--------------------------------------------------------------------------------
/public/fonts/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fre-ben/sisu/HEAD/public/fonts/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged --config .lintstagedrc.json
5 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json server/index.ts",
4 | "ext": "js ts"
5 | }
6 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.css": "stylelint --fix",
3 | "*.{js,jsx,ts,tsx}": "eslint --cache --fix",
4 | "*.{js,jsx,ts,tsx,css,md,json}": "prettier --write"
5 | }
6 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 |
3 | export const parameters = {
4 | actions: { argTypesRegex: "^on[A-Z].*" },
5 | layout: "centered",
6 | };
7 |
--------------------------------------------------------------------------------
/components/misc/PlayerCount.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 | grid-auto-flow: column;
4 | align-items: center;
5 | column-gap: 0.5em;
6 | font-size: 1.7em;
7 | font-weight: 500;
8 | }
9 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # next.js
10 | /.next/
11 | /out/
12 |
13 | # production
14 | /build
15 | /storybook-static
16 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # next.js
10 | /.next/
11 | /out/
12 |
13 | # production
14 | /build
15 | /storybook-static
16 |
--------------------------------------------------------------------------------
/components/misc/LobbyListItem.module.css:
--------------------------------------------------------------------------------
1 | .listItem {
2 | display: grid;
3 | grid-auto-flow: column;
4 | align-items: center;
5 | column-gap: 6em;
6 | }
7 |
8 | .lobbyNr {
9 | font-size: 2.7em;
10 | font-weight: 500;
11 | }
12 |
--------------------------------------------------------------------------------
/components/button/RulesBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 |
3 | function RulesBtn() {
4 | return (
5 | How to play
6 | );
7 | }
8 |
9 | export default RulesBtn;
10 |
--------------------------------------------------------------------------------
/components/button/StartGameBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 |
3 | function StartGameBtn() {
4 | return (
5 | Start Game
6 | );
7 | }
8 |
9 | export default StartGameBtn;
10 |
--------------------------------------------------------------------------------
/components/logo/Logo.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Logo.module.css";
2 |
3 | type LogoProps = {
4 | size: string;
5 | };
6 |
7 | function Logo({ size }: LogoProps) {
8 | return
Sisu
;
9 | }
10 |
11 | export default Logo;
12 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "build",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "include": ["server/**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/lib/functions.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | export function getPlayerName(): string {
4 | return localStorage.getItem("playerName");
5 | }
6 |
7 | export function getLobbyNr(): number {
8 | const router = useRouter();
9 | return +router.query.lobby;
10 | }
11 |
--------------------------------------------------------------------------------
/components/button/ConfirmBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 |
3 | function ConfirmBtn() {
4 | return (
5 |
6 | Confirm
7 |
8 | );
9 | }
10 |
11 | export default ConfirmBtn;
12 |
--------------------------------------------------------------------------------
/components/logo/Logo.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react/types-6-0";
2 | import Logo from "./Logo";
3 |
4 | export default {
5 | title: "Common/Logo",
6 | } as Meta;
7 |
8 | export const big = () => ;
9 | export const small = () => ;
10 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: [
3 | "../components/**/*.stories.mdx",
4 | "../components/**/*.stories.@(js|jsx|ts|tsx)",
5 | ],
6 | addons: [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "storybook-css-modules-preset",
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/components/gameboard/DrawPilePrompt.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 | row-gap: 1.5em;
4 | justify-items: center;
5 | min-width: 16em;
6 | }
7 |
8 | .buttons {
9 | display: grid;
10 | grid-auto-flow: column;
11 | column-gap: 1em;
12 | }
13 |
14 | .card {
15 | width: 6.5em;
16 | cursor: default;
17 | }
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue--user-story-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue (User Story)
3 | about: Issue in form of a user story
4 | title: ""
5 | labels: user story
6 | assignees: ""
7 | ---
8 |
9 | ## User story
10 |
11 | **As a**:
12 | **I want to**:
13 | **So I can**:
14 |
15 | ## Acceptance criteria
16 |
17 | - [ ]
18 | - [ ]
19 | - [ ]
20 |
--------------------------------------------------------------------------------
/components/button/BackBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 |
3 | function BackBtn() {
4 | return (
5 |
8 |
9 | Back
10 |
11 | );
12 | }
13 |
14 | export default BackBtn;
15 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 3% 10%;
8 | }
9 |
10 | .menuItems {
11 | margin-top: 6em;
12 | display: grid;
13 | grid-auto-flow: row;
14 | row-gap: 1.5em;
15 | justify-items: center;
16 | }
17 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { AppProps } from "next/app";
2 | import "../styles/globals.css";
3 | import { SocketContextProvider } from "../contexts/SocketContext";
4 |
5 | function App({ Component, pageProps }: AppProps) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/styles/User.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 3% 10%;
8 | }
9 |
10 | .pageItems {
11 | margin-top: 6em;
12 | display: grid;
13 | grid-auto-flow: column;
14 | column-gap: 3em;
15 | }
16 |
17 | .form {
18 | display: grid;
19 | justify-items: end;
20 | }
21 |
--------------------------------------------------------------------------------
/components/button/types.ts:
--------------------------------------------------------------------------------
1 | import { MouseEventHandler } from "react";
2 |
3 | export type ButtonProps = {
4 | onClick?: MouseEventHandler;
5 | };
6 |
7 | export type JoinBtnProps = {
8 | onClick: MouseEventHandler;
9 | lobbyIsFull: boolean;
10 | hasStarted: boolean;
11 | };
12 |
13 | export type KeepDiscardBtnProps = {
14 | handleClick: MouseEventHandler;
15 | };
16 |
--------------------------------------------------------------------------------
/components/button/ExitBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import type { ButtonProps } from "./types";
3 |
4 | function ExitBtn({ onClick }: ButtonProps) {
5 | return (
6 |
10 |
11 | Exit
12 |
13 | );
14 | }
15 |
16 | export default ExitBtn;
17 |
--------------------------------------------------------------------------------
/components/button/Continue.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import { ButtonProps } from "./types";
3 |
4 | function ContinueBtn({ onClick }: ButtonProps) {
5 | return (
6 |
7 |
8 | Continue
9 |
10 |
11 | );
12 | }
13 |
14 | export default ContinueBtn;
15 |
--------------------------------------------------------------------------------
/components/button/CreateBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import { ButtonProps } from "./types";
3 |
4 | function CreateBtn({ onClick }: ButtonProps) {
5 | return (
6 |
7 |
8 | Create Lobby
9 |
10 |
11 | );
12 | }
13 |
14 | export default CreateBtn;
15 |
--------------------------------------------------------------------------------
/components/input/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from "@storybook/react/types-6-0";
2 | import NameInput from "./NameInput";
3 |
4 | export default {
5 | title: "Common/Input",
6 | } as Meta;
7 |
8 | const Template: Story = (args) => (
9 |
15 | );
16 |
17 | export const Name = Template.bind({});
18 |
19 | Name.args = {};
20 |
--------------------------------------------------------------------------------
/server/lib/turnPhases.ts:
--------------------------------------------------------------------------------
1 | export const phase = {
2 | DRAWDECISION: "drawDecision",
3 | DRAWPILEDECISION: "drawPileDecision",
4 | DRAWPILEDISCARD: "drawPileDiscard",
5 | DRAWPILEKEEP: "drawPileKeep",
6 | DISCARDPILEDECISION: "discardPileDecision",
7 | DISCARDPILEREPLACEOPEN: "discardPileReplaceOpen",
8 | DISCARDPILEREPLACEHIDDEN: "discardPileReplaceHidden",
9 | WAITTURN: "waitTurn",
10 | WAITTURNDRAWPILEDECISION: "waitTurnDrawPileDecision",
11 | };
12 |
--------------------------------------------------------------------------------
/components/button/KeepBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import { KeepDiscardBtnProps } from "./types";
3 |
4 | function KeepBtn({ handleClick }: KeepDiscardBtnProps) {
5 | return (
6 |
10 | Keep
11 |
12 | );
13 | }
14 |
15 | export default KeepBtn;
16 |
--------------------------------------------------------------------------------
/components/button/DiscardBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import { KeepDiscardBtnProps } from "./types";
3 |
4 | function DiscardBtn({ handleClick }: KeepDiscardBtnProps) {
5 | return (
6 |
10 | Discard
11 |
12 | );
13 | }
14 |
15 | export default DiscardBtn;
16 |
--------------------------------------------------------------------------------
/components/button/ReadyBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import { ButtonProps } from "./types";
3 |
4 | function ReadyBtn({ onClick }: ButtonProps) {
5 | return (
6 |
10 |
11 | Ready? Start Game
12 |
13 |
14 | );
15 | }
16 |
17 | export default ReadyBtn;
18 |
--------------------------------------------------------------------------------
/public/arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/button/RestartBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import { ButtonProps } from "./types";
3 |
4 | function RestartBtn({ onClick }: ButtonProps) {
5 | return (
6 |
10 |
11 | Again? Restart Game
12 |
13 |
14 | );
15 | }
16 |
17 | export default RestartBtn;
18 |
--------------------------------------------------------------------------------
/components/misc/PlayerCount.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./PlayerCount.module.css";
2 |
3 | export type PlayerCountProps = {
4 | playerCount: number;
5 | };
6 |
7 | function PlayerCount({ playerCount }: PlayerCountProps) {
8 | const maxPlayers = 8;
9 |
10 | return (
11 |
12 |
13 | {playerCount}/{maxPlayers}
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export default PlayerCount;
21 |
--------------------------------------------------------------------------------
/public/arrow_right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/gameboard/types.ts:
--------------------------------------------------------------------------------
1 | import { MouseEventHandler } from "react";
2 | import { ActivePlayer, Card } from "../../server/lib/gameTypes";
3 |
4 | export type DiscardPileProps = {
5 | onPileClick?: MouseEventHandler;
6 | card: Card;
7 | turnPhase: string;
8 | };
9 |
10 | export type DrawPileProps = Pick;
11 |
12 | export type RoundCounterProps = {
13 | roundNr: number;
14 | };
15 |
16 | export type StatusbarProps = {
17 | activePlayer: ActivePlayer;
18 | };
19 |
--------------------------------------------------------------------------------
/styles/Rules.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | height: 100vh;
3 | display: grid;
4 | place-items: center;
5 | grid-template-rows: 1fr 70%;
6 | row-gap: 2.2em;
7 | padding: 3% 10%;
8 | }
9 |
10 | .menuItems {
11 | display: grid;
12 | grid-auto-flow: column;
13 | align-items: start;
14 | column-gap: 3.5em;
15 | height: 100%;
16 | width: 90%;
17 | }
18 |
19 | .rules {
20 | font-size: 1.5em;
21 | overflow-y: auto;
22 | max-height: 100%;
23 | padding-right: 1em;
24 | }
25 |
26 | .rules p {
27 | margin-top: 0;
28 | }
29 |
--------------------------------------------------------------------------------
/components/button/JoinBtn.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Button.module.css";
2 | import { JoinBtnProps } from "./types";
3 |
4 | function JoinBtn({ onClick, lobbyIsFull, hasStarted }: JoinBtnProps) {
5 | return (
6 |
11 |
12 | Join
13 |
14 |
15 | );
16 | }
17 |
18 | export default JoinBtn;
19 |
--------------------------------------------------------------------------------
/public/players.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve"
16 | },
17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/components/logo/Logo.module.css:
--------------------------------------------------------------------------------
1 | .logo {
2 | line-height: 1em;
3 | background-image: linear-gradient(
4 | var(--secondary-color),
5 | var(--secondary-color)
6 | ),
7 | linear-gradient(var(--secondary-color), var(--secondary-color));
8 | background-repeat: no-repeat;
9 | background-size: 3px 55%, 64% 3px;
10 | background-position: right bottom, right bottom;
11 | user-select: none;
12 | }
13 |
14 | .big {
15 | font-weight: 900;
16 | font-size: 8.5em;
17 | padding: 0 0.1em;
18 | }
19 |
20 | .small {
21 | font-weight: 600;
22 | font-size: 4.2em;
23 | padding: 0 0.11em;
24 | }
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 | /storybook-static
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 | .eslintcache
23 | *:Zone.Identifier
24 | api.json
25 |
26 | # debug
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # local env files
32 | .env.local
33 | .env.development.local
34 | .env.test.local
35 | .env.production.local
36 |
37 | # vercel
38 | .vercel
39 |
--------------------------------------------------------------------------------
/styles/Lobbies.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 3% 10%;
8 | }
9 |
10 | .container button:nth-child(2) {
11 | margin: 1em 0;
12 | padding-left: 1em;
13 | }
14 |
15 | .pageItems {
16 | margin-top: 1em;
17 | display: grid;
18 | grid-auto-flow: column;
19 | column-gap: 3em;
20 | min-width: 800px;
21 | }
22 |
23 | .list {
24 | margin: 0;
25 | padding: 0 1em 0 0;
26 | overflow-y: auto;
27 | max-height: 18em;
28 | min-height: 18em;
29 | }
30 | .list li {
31 | margin-bottom: 1em;
32 | }
33 |
--------------------------------------------------------------------------------
/components/pages/Pages.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react/types-6-0";
2 | import { SocketContextProvider } from "../../contexts/SocketContext";
3 | import Index from "../../pages/index";
4 | import Lobbies from "../../pages/lobbies";
5 | import Rules from "../../pages/rules";
6 | import User from "../../pages/user";
7 |
8 | export default {
9 | title: "Pages/Main Menu",
10 | parameters: { layout: "fullscreen" },
11 | } as Meta;
12 |
13 | export const index = () => ;
14 | export const rules = () => ;
15 | export const user = () => ;
16 | export const lobbies = () => (
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "prettier"
12 | ],
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": {
15 | "ecmaFeatures": {
16 | "jsx": true
17 | },
18 | "ecmaVersion": 12,
19 | "sourceType": "module"
20 | },
21 | "plugins": ["react", "@typescript-eslint"],
22 | "rules": {
23 | "react/react-in-jsx-scope": "off",
24 | "@typescript-eslint/explicit-module-boundary-types": "off"
25 | },
26 | "settings": {
27 | "react": {
28 | "version": "detect"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/components/misc/Misc.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from "@storybook/react/types-6-0";
2 | import LobbyListItem, { LobbyListItemProps } from "./LobbyListItem";
3 | import PlayerCount, { PlayerCountProps } from "./PlayerCount";
4 |
5 | export default {
6 | title: "Common/Misc",
7 | } as Meta;
8 |
9 | const Template: Story = (args) => ;
10 | const TemplateListItem: Story = (args) => (
11 |
12 | );
13 |
14 | export const Playercounter = Template.bind({});
15 |
16 | Playercounter.args = {
17 | playerCount: 0,
18 | };
19 |
20 | export const Lobbylistitem = TemplateListItem.bind({});
21 |
22 | Lobbylistitem.args = {
23 | playerCount: 4,
24 | lobbyNr: 1,
25 | onClick: null,
26 | lobbyIsFull: false,
27 | };
28 |
--------------------------------------------------------------------------------
/contexts/SocketContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, ReactNode, useEffect, useState } from "react";
2 | import { io, Socket } from "socket.io-client";
3 |
4 | type SocketContextType = {
5 | socket: Socket;
6 | };
7 |
8 | export const SocketContext = createContext(null);
9 |
10 | type SocketContextProviderProps = {
11 | children: ReactNode;
12 | };
13 |
14 | export function SocketContextProvider({
15 | children,
16 | }: SocketContextProviderProps) {
17 | const [socket, setSocket] = useState(null);
18 |
19 | useEffect(() => {
20 | const newSocket = io();
21 |
22 | newSocket.on("connect", () => {
23 | setSocket(newSocket);
24 | console.log(newSocket.id + " connected");
25 | });
26 | }, []);
27 |
28 | return (
29 |
30 | {children}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [main]
9 | pull_request:
10 | branches: [main]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [10.x, 12.x, 14.x, 15.x]
19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm ci
28 | - run: npm run build --if-present
29 | - run: npm test
30 |
--------------------------------------------------------------------------------
/components/input/NameInput.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Input.module.css";
2 |
3 | type NameInputProps = {
4 | isMaxLength: boolean;
5 | playerName: string;
6 | onHandleChange(event): void;
7 | };
8 |
9 | function NameInput({
10 | isMaxLength,
11 | playerName,
12 | onHandleChange,
13 | }: NameInputProps) {
14 | return (
15 | <>
16 |
17 |
onHandleChange(event)}
23 | minLength={2}
24 | required
25 | value={playerName}
26 | autoFocus={true}
27 | autoComplete="off"
28 | autoCorrect="off"
29 | />
30 | {isMaxLength && (
31 |
Character limit reached!
32 | )}
33 |
34 | >
35 | );
36 | }
37 |
38 | export default NameInput;
39 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 | import RulesBtn from "../components/button/RulesBtn";
4 | import StartGameBtn from "../components/button/StartGameBtn";
5 | import Logo from "../components/logo/Logo";
6 | import styles from "../styles/Home.module.css";
7 |
8 | export default function Index() {
9 | return (
10 | <>
11 |
12 | Sisu
13 |
14 |
15 |
16 |
17 |
32 |
33 | >
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/gameboard/Piles.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 | justify-items: center;
4 | row-gap: 1em;
5 | }
6 |
7 | .drawHeadline {
8 | background-size: 3px 58%, 81% 3px;
9 | }
10 |
11 | .discardHeadline {
12 | background-size: 3px 58%, 84.5% 3px;
13 | }
14 |
15 | .card {
16 | width: 6.5em;
17 | padding: 0.5em 0.5em;
18 | background-image: linear-gradient(
19 | var(--secondary-color),
20 | var(--secondary-color)
21 | ),
22 | linear-gradient(var(--secondary-color), var(--secondary-color)),
23 | linear-gradient(var(--secondary-color), var(--secondary-color)),
24 | linear-gradient(var(--secondary-color), var(--secondary-color));
25 | background-size: 3px 20%, 37% 3px;
26 | background-repeat: no-repeat;
27 | background-position: right bottom, right bottom, left top, left top;
28 | transition: var(--bottom-transition);
29 | cursor: pointer;
30 | }
31 |
32 | .notClickable {
33 | cursor: default;
34 | }
35 |
36 | .card:hover {
37 | background-size: 3px 20%, 47% 3px;
38 | }
39 |
--------------------------------------------------------------------------------
/components/gameboard/RoundCounter.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import { SocketContext } from "../../contexts/SocketContext";
3 | import { getLobbyNr } from "../../lib/functions";
4 | import styles from "./MiscElements.module.css";
5 | import type { RoundCounterProps } from "./types";
6 |
7 | function RoundCounter() {
8 | const { socket } = useContext(SocketContext);
9 | const [roundNr, setRoundNr] = useState(null);
10 | const lobbyNr = getLobbyNr();
11 |
12 | useEffect(() => {
13 | if (!socket) {
14 | return;
15 | }
16 |
17 | function handleDisplayRoundNr(round) {
18 | setRoundNr(round);
19 | }
20 |
21 | socket.on("display rounds", handleDisplayRoundNr);
22 | socket.emit("get rounds to display", lobbyNr);
23 | }, [socket, roundNr]);
24 |
25 | return (
26 | <>
27 |
28 | Round:{roundNr}
29 |
30 | >
31 | );
32 | }
33 |
34 | export default RoundCounter;
35 |
--------------------------------------------------------------------------------
/components/gameboard/OpponentCardGrid.tsx:
--------------------------------------------------------------------------------
1 | import type { Card } from "../../server/lib/gameTypes";
2 | import styles from "./CardGrid.module.css";
3 |
4 | export type CardGridProps = {
5 | cards: Card[];
6 | name: string;
7 | roundScore: number;
8 | };
9 |
10 | export type PlayerCardGridProps = {
11 | cards: Card[];
12 | name: string;
13 | roundScore: number;
14 | gameHasStarted: boolean;
15 | turnPhase: string;
16 | };
17 |
18 | function OpponentCardGrid({ cards, name, roundScore }: CardGridProps) {
19 | const createPlayerCardGrid = cards.map((card, index) => (
20 |
24 | ));
25 |
26 | return (
27 |
28 |
29 | Score: {roundScore}
30 |
31 | {name}
32 | {createPlayerCardGrid}
33 |
34 | );
35 | }
36 |
37 | export default OpponentCardGrid;
38 |
--------------------------------------------------------------------------------
/components/input/Input.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 | grid-auto-flow: row;
4 | justify-items: end;
5 | align-items: start;
6 | min-height: 105px;
7 | }
8 |
9 | .input {
10 | background: none;
11 | border: none;
12 | width: 8em;
13 | font-size: 2.7em;
14 | font-weight: 600;
15 | text-align: end;
16 | color: var(--text-color);
17 | line-height: 1em;
18 | background-image: linear-gradient(
19 | var(--secondary-color),
20 | var(--secondary-color)
21 | ),
22 | linear-gradient(var(--secondary-color), var(--secondary-color));
23 | background-repeat: no-repeat;
24 | background-size: 3px 58%, 27% 3px;
25 | background-position: right bottom, right bottom;
26 | padding: 0 0.2em 0.2em 0;
27 | transition: var(--bottom-transition);
28 | }
29 |
30 | .input::placeholder {
31 | color: var(--placeholder-color);
32 | }
33 |
34 | .input:hover {
35 | background-size: 3px 58%, calc(27% + 10%) 3px;
36 | }
37 |
38 | .status {
39 | color: var(--secondary-color);
40 | font-size: 1.2em;
41 | margin: 0;
42 | }
43 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/components/gameboard/Score.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 | justify-items: center;
4 | row-gap: 1em;
5 | }
6 |
7 | .container ul {
8 | width: 85%;
9 | margin: 0;
10 | padding: 0.5em 0.7em;
11 | background-image: linear-gradient(
12 | var(--secondary-color),
13 | var(--secondary-color)
14 | ),
15 | linear-gradient(var(--secondary-color), var(--secondary-color)),
16 | linear-gradient(var(--secondary-color), var(--secondary-color)),
17 | linear-gradient(var(--secondary-color), var(--secondary-color));
18 | background-size: 3px 20%, 25% 3px;
19 | background-repeat: no-repeat;
20 | background-position: right bottom, right bottom, left top, left top;
21 | }
22 |
23 | .totalScoreHeadline {
24 | background-size: 3px 58%, 86% 3px;
25 | }
26 |
27 | .playerListItem {
28 | margin: 0 auto;
29 | display: grid;
30 | grid-template-columns: 1fr 1fr;
31 | width: 100%;
32 | font-size: 1.8em;
33 | }
34 |
35 | .playerScore {
36 | color: var(--secondary-color);
37 | justify-self: end;
38 | }
39 |
40 | .playerName {
41 | justify-self: start;
42 | }
43 |
--------------------------------------------------------------------------------
/components/misc/LobbyListItem.tsx:
--------------------------------------------------------------------------------
1 | import { MouseEventHandler } from "react";
2 | import JoinBtn from "../button/JoinBtn";
3 | import styles from "./LobbyListItem.module.css";
4 | import PlayerCount from "./PlayerCount";
5 |
6 | export type LobbyListItemProps = {
7 | playerCount: number;
8 | lobbyNr: number;
9 | onClick: MouseEventHandler;
10 | lobbyIsFull: boolean;
11 | hasStarted: boolean;
12 | };
13 |
14 | function LobbyListItem({
15 | playerCount,
16 | lobbyNr,
17 | onClick,
18 | lobbyIsFull,
19 | hasStarted,
20 | }: LobbyListItemProps) {
21 | if (playerCount >= 8) {
22 | lobbyIsFull = true;
23 | }
24 |
25 | if (playerCount <= 0) {
26 | playerCount = 0;
27 | }
28 |
29 | return (
30 |
31 | Lobby #{lobbyNr}
32 |
33 |
38 |
39 | );
40 | }
41 |
42 | export default LobbyListItem;
43 |
--------------------------------------------------------------------------------
/server/lib/statusMessages.ts:
--------------------------------------------------------------------------------
1 | export const status = {
2 | PREREADY: "Start game if all players are connected",
3 | PRESTART: "Reveal two hidden cards on your grid to start the round",
4 | PRESTARTWAIT: "Wait for other players to reveal two cards",
5 | WAITTURN: (activePlayerName: string) => {
6 | return `It's ${activePlayerName}'s turn. Please wait`;
7 | },
8 | DRAWDECISION: "Draw card from draw pile or discard pile",
9 | DRAWPILEDECISION: "Do you want to keep or discard your drawn card?",
10 | DRAWPILEDISCARD:
11 | "Card added to discard pile. Reveal a hidden card on your grid",
12 | DRAWPILEDISCARDINVALID:
13 | "Invalid card clicked. Please reveal a hidden card on your grid",
14 | DRAWDISCARDPILEKEEP: "Replace open or hidden card on your grid",
15 | DRAWDISCARDPILEKEEPOPEN:
16 | "Replaced open card. Replaced card added to discard pile",
17 | DRAWDISCARDPILEKEEPHIDDEN:
18 | "Replaced hidden card. Replaced card added to discard pile",
19 | ROUNDEND: "Round ended! Press continue to get to the next round",
20 | GAMEEND: `${null} lost (reached total score of ${null}). Game ended!`,
21 | };
22 |
--------------------------------------------------------------------------------
/components/gameboard/RoundScoreModal.tsx:
--------------------------------------------------------------------------------
1 | import ContinueBtn from "../button/Continue";
2 | import styles from "./MiscElements.module.css";
3 |
4 | function RoundScoreModal(onClick) {
5 | // Example data, will be removed later by Props handed down to CardGrid
6 | const players = [
7 | { name: "Frederik", score: 20 },
8 | { name: "Kalle", score: 10 },
9 | { name: "Eva", score: 30 },
10 | { name: "Louis", score: 60 },
11 | { name: "Boris", score: 20 },
12 | { name: "Kalle", score: 10 },
13 | { name: "Eva", score: 30 },
14 | { name: "Peter", score: 30 },
15 | ];
16 |
17 | const createRoundScoreList = players.map((player) => (
18 |
19 | {player.name} {" "}
20 | {player.score}
21 |
22 | ));
23 |
24 | return (
25 |
26 |
Round Score
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default RoundScoreModal;
34 |
--------------------------------------------------------------------------------
/components/gameboard/Statusbar.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import { SocketContext } from "../../contexts/SocketContext";
3 | import { getLobbyNr } from "../../lib/functions";
4 | import { status } from "../../server/lib/statusMessages";
5 | import styles from "./MiscElements.module.css";
6 | import { StatusbarProps } from "./types";
7 |
8 | function Statusbar({ activePlayer }: StatusbarProps) {
9 | const { socket } = useContext(SocketContext);
10 | const lobbyNr = getLobbyNr();
11 | const [statusMessage, setStatusMessage] = useState(status.PREREADY);
12 |
13 | useEffect(() => {
14 | if (!socket || !lobbyNr) {
15 | return;
16 | }
17 |
18 | function handleDisplayStatus(statusMessage: string) {
19 | setStatusMessage(statusMessage);
20 | }
21 |
22 | socket.on("display status", handleDisplayStatus);
23 | return () => {
24 | socket.off("display status", handleDisplayStatus);
25 | };
26 | }, [socket, lobbyNr, activePlayer]);
27 |
28 | return (
29 | <>
30 |
31 |
32 | {statusMessage}
33 |
34 | >
35 | );
36 | }
37 |
38 | export default Statusbar;
39 |
--------------------------------------------------------------------------------
/server/lib/gameTypes.ts:
--------------------------------------------------------------------------------
1 | export type Game = {
2 | lobbyNr: number;
3 | roundNr: number;
4 | playerCount: number;
5 | lobbyIsFull: boolean;
6 | hasStarted: boolean;
7 | activePlayerIndex: number;
8 | players: Player[];
9 | drawPileCards: Card[];
10 | tempDrawPileCard: Card;
11 | discardPileCards: Card[];
12 | currentRoundScores: CurrentRoundScore[];
13 | };
14 |
15 | export type GamesType = {
16 | [lobbyNr: number]: Game;
17 | };
18 |
19 | export type GameForLobby = Pick<
20 | Game,
21 | "lobbyNr" | "playerCount" | "lobbyIsFull" | "hasStarted"
22 | >;
23 |
24 | export type Player = {
25 | name: string;
26 | socketID: string;
27 | isReady: boolean;
28 | cards: Card[];
29 | totalScore: number;
30 | roundScore: number[];
31 | allCardsRevealed: boolean;
32 | };
33 |
34 | export type PlayerForCardGrid = Pick<
35 | Player,
36 | "name" | "cards" | "roundScore" | "socketID" | "isReady"
37 | >;
38 |
39 | export type PlayerScoreList = Pick;
40 |
41 | export type PlayerRoundScore = Pick;
42 |
43 | export type ActivePlayer = Pick;
44 |
45 | export type CurrentRoundScore = Pick;
46 |
47 | export type Card = {
48 | id?: number;
49 | value: number;
50 | imgSrc: string;
51 | hidden: boolean;
52 | };
53 |
54 | export type CardToGenerate = [Card, number];
55 |
--------------------------------------------------------------------------------
/components/gameboard/DiscardPile.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { SocketContext } from "../../contexts/SocketContext";
3 | import { getLobbyNr } from "../../lib/functions";
4 | import styles from "./Piles.module.css";
5 | import type { DiscardPileProps } from "./types";
6 |
7 | function DiscardPile({ turnPhase, card }: DiscardPileProps) {
8 | const { socket } = useContext(SocketContext);
9 | const lobbyNr = getLobbyNr();
10 | const notClickable = `${styles.card} ${styles.notClickable}`;
11 |
12 | function handlePileClick() {
13 | switch (turnPhase) {
14 | case "drawDecision":
15 | socket.emit("DRAWDECISION: click discardpile", socket.id, lobbyNr);
16 | break;
17 | case "waitTurn":
18 | return;
19 | default:
20 | return;
21 | }
22 | }
23 |
24 | function cardStyle() {
25 | switch (turnPhase) {
26 | case "drawDecision":
27 | return styles.card;
28 | case "waitTurn":
29 | return notClickable;
30 | default:
31 | return notClickable;
32 | }
33 | }
34 |
35 | return (
36 | <>
37 |
38 | Discard Pile
39 |
44 |
45 | >
46 | );
47 | }
48 |
49 | export default DiscardPile;
50 |
--------------------------------------------------------------------------------
/components/gameboard/TotalScore.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import { SocketContext } from "../../contexts/SocketContext";
3 | import { getLobbyNr } from "../../lib/functions";
4 | import type { PlayerScoreList } from "../../server/lib/gameTypes";
5 | import styles from "./Score.module.css";
6 | type ScoreListProps = {
7 | name: string;
8 | totalScore: number;
9 | };
10 |
11 | function TotalScore() {
12 | const { socket } = useContext(SocketContext);
13 | const [scoreList, setScoreList] = useState([]);
14 | const lobbyNr = getLobbyNr();
15 |
16 | const renderScoreList = scoreList.map(({ name, totalScore }) => {
17 | return (
18 |
19 | {name}
20 | {totalScore}
21 |
22 | );
23 | });
24 |
25 | useEffect(() => {
26 | if (!socket || !lobbyNr) {
27 | return;
28 | }
29 | function handleDisplayScores(scores: PlayerScoreList[]) {
30 | setScoreList(scores);
31 | }
32 |
33 | socket.on("display scores", handleDisplayScores);
34 | socket.emit("get scores to display", lobbyNr);
35 | }, [socket, lobbyNr]);
36 |
37 | return (
38 | <>
39 |
40 | Total Score
41 |
42 |
43 | >
44 | );
45 | }
46 |
47 | export default TotalScore;
48 |
--------------------------------------------------------------------------------
/components/gameboard/DrawPile.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { SocketContext } from "../../contexts/SocketContext";
3 | import { getLobbyNr } from "../../lib/functions";
4 | import { phase } from "../../server/lib/turnPhases";
5 | import styles from "./Piles.module.css";
6 | import type { DrawPileProps } from "./types";
7 |
8 | function DrawPile({ turnPhase }: DrawPileProps) {
9 | const { socket } = useContext(SocketContext);
10 | const lobbyNr = getLobbyNr();
11 | const notClickable = `${styles.card} ${styles.notClickable}`;
12 |
13 | function handlePileClick() {
14 | switch (turnPhase) {
15 | case phase.DRAWDECISION:
16 | socket.emit("DRAWDECISION: click drawpile", socket.id, lobbyNr);
17 | socket.emit("get new drawpilecard", lobbyNr);
18 | break;
19 | case "waitTurn":
20 | return;
21 | default:
22 | return;
23 | }
24 | }
25 |
26 | function cardStyle() {
27 | switch (turnPhase) {
28 | case phase.DRAWDECISION:
29 | return styles.card;
30 | case "waitTurn":
31 | return notClickable;
32 | default:
33 | return notClickable;
34 | }
35 | }
36 |
37 | return (
38 | <>
39 |
40 | Draw Pile
41 |
46 |
47 | >
48 | );
49 | }
50 |
51 | export default DrawPile;
52 |
--------------------------------------------------------------------------------
/styles/Game.module.css:
--------------------------------------------------------------------------------
1 | .viewContainer {
2 | min-height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 2% 2%;
8 | }
9 |
10 | .topBar {
11 | display: grid;
12 | grid-auto-flow: column;
13 | align-items: center;
14 | column-gap: 2em;
15 | justify-items: center;
16 | grid-area: topbar;
17 | }
18 |
19 | .sideBar {
20 | display: grid;
21 | grid-auto-flow: row;
22 | justify-items: center;
23 | row-gap: 2em;
24 | grid-area: sidebar;
25 | }
26 |
27 | .pageElements {
28 | display: grid;
29 | grid-template-columns: 80% 20%;
30 | grid-template-rows: auto 1fr;
31 | align-items: start;
32 | column-gap: 1em;
33 | width: 100%;
34 | grid-template-areas:
35 | "topbar sidebar"
36 | "game sidebar";
37 | }
38 |
39 | .gameElements {
40 | display: grid;
41 | justify-items: center;
42 | grid-area: game;
43 | grid-template-rows: auto auto;
44 | grid-template-columns: auto 30%;
45 | grid-template-areas: "opp opp";
46 | }
47 |
48 | .opponents {
49 | margin-top: 1em;
50 | display: grid;
51 | grid-template-columns: repeat(6, 1fr);
52 | justify-items: center;
53 | align-items: start;
54 | column-gap: 1em;
55 | row-gap: 1.5em;
56 | min-height: 230px;
57 | grid-area: opp;
58 | }
59 |
60 | .playerCardGridDrawPilePrompt {
61 | display: grid;
62 | justify-self: end;
63 | align-items: center;
64 | }
65 |
66 | .playerCardGrid {
67 | justify-self: end;
68 | padding-right: 3em;
69 | }
70 |
71 | .playerCardGrid8 {
72 | position: relative;
73 | bottom: 10em;
74 | }
75 |
--------------------------------------------------------------------------------
/components/gameboard/MiscElements.module.css:
--------------------------------------------------------------------------------
1 | .roundCounter {
2 | display: grid;
3 | grid-auto-flow: column;
4 | margin: 0;
5 | column-gap: 0.3em;
6 | font-size: 1.8em;
7 | font-weight: 500;
8 | padding: 0.15em 0.3em;
9 | line-height: 1em;
10 | background-image: linear-gradient(
11 | var(--secondary-color),
12 | var(--secondary-color)
13 | ),
14 | linear-gradient(var(--secondary-color), var(--secondary-color));
15 | background-size: 3px 58%, 78% 3px;
16 | background-repeat: no-repeat;
17 | background-position: right bottom, right bottom;
18 | }
19 |
20 | .roundCounter span {
21 | color: var(--secondary-color);
22 | }
23 |
24 | .statusbar {
25 | display: grid;
26 | margin: 0;
27 | grid-auto-flow: column;
28 | align-items: center;
29 | column-gap: 0.3em;
30 | font-size: 1.4em;
31 | font-weight: 500;
32 | color: var(--secondary-color);
33 | }
34 |
35 | .modalContainer {
36 | display: grid;
37 | justify-items: center;
38 | background-color: var(--primary-color);
39 | padding: 1em 2em;
40 | box-shadow: var(--shadow);
41 | border-radius: 15px;
42 | }
43 |
44 | .modalContainer p {
45 | font-size: 2.7em;
46 | font-weight: 600;
47 | margin: 0;
48 | }
49 |
50 | .scoreList {
51 | padding: 0;
52 | margin-top: 1.5em;
53 | margin-bottom: 1em;
54 | font-weight: 500;
55 | }
56 |
57 | .playerListItem {
58 | display: grid;
59 | grid-template-columns: 1fr 1fr;
60 | font-size: 1.8em;
61 | }
62 |
63 | .playerScore {
64 | color: var(--secondary-color);
65 | justify-self: end;
66 | }
67 |
68 | .playerName {
69 | justify-self: start;
70 | }
71 |
--------------------------------------------------------------------------------
/components/button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmBtn from "./ConfirmBtn";
2 | import { Meta, Story } from "@storybook/react/types-6-0";
3 | import BackBtn from "./BackBtn";
4 | import RulesBtn from "./RulesBtn";
5 | import StartGameBtn from "./StartGameBtn";
6 | import { JoinBtnProps } from "./types";
7 | import JoinBtn from "./JoinBtn";
8 | import CreateBtn from "./CreateBtn";
9 | import KeepBtn from "./KeepBtn";
10 | import DiscardBtn from "./DiscardBtn";
11 | import ReadyBtn from "./ReadyBtn";
12 | import ExitBtn from "./ExitBtn";
13 | import RestartBtn from "./RestartBtn";
14 | import ContinueBtn from "./Continue";
15 |
16 | export default {
17 | title: "Common/Button",
18 | } as Meta;
19 |
20 | const TemplateJoin: Story = (args) => ;
21 |
22 | export const Join = TemplateJoin.bind({});
23 |
24 | Join.args = {
25 | onClick: () => alert("Hello"),
26 | lobbyIsFull: false,
27 | hasStarted: false,
28 | };
29 |
30 | export const back = () => ;
31 | export const startgame = () => ;
32 | export const rules = () => ;
33 | export const confirm = () => ;
34 | export const create = () => alert("test")} />;
35 | export const keep = () => ;
36 | export const discard = () => ;
37 | export const ready = () => alert("test")} />;
38 | export const exit = () => alert("test")} />;
39 | export const restart = () => alert("test")} />;
40 | export const cont = () => alert("test")} />;
41 |
--------------------------------------------------------------------------------
/components/gameboard/GameBoard.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react/types-6-0";
2 | import CardGrid from "./CardGrid";
3 | import DiscardPile from "./DiscardPile";
4 | import DrawPile from "./DrawPile";
5 | import OpponentCardGrid from "./OpponentCardGrid";
6 | import RoundScoreModal from "./RoundScoreModal";
7 | import RoundCounter from "./RoundCounter";
8 | import Statusbar from "./Statusbar";
9 | import TotalScore from "./TotalScore";
10 | import { SocketContextProvider } from "../../contexts/SocketContext";
11 | import DrawPilePrompt from "./DrawPilePrompt";
12 |
13 | export default {
14 | title: "Common/Gameboard",
15 | } as Meta;
16 |
17 | export const drawpile = () => ;
18 | export const discardpile = () => ;
19 | export const totalscore = () => ;
20 | export const cardgrid = () => (
21 |
28 | );
29 | export const opponentcardgrid = () => (
30 |
31 | );
32 | export const roundscoremodal = () => ;
33 | export const roundcounter = () => (
34 |
35 |
36 |
37 | );
38 | export const statusbar = () => (
39 |
40 |
41 |
42 | );
43 | export const drawpileprompt = () => (
44 |
45 |
46 |
47 | );
48 |
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | import { createServer } from "http";
2 | import { parse } from "url";
3 | import next from "next";
4 | import path from "path";
5 | import fs from "fs/promises";
6 | import { listenSocket } from "./socket";
7 |
8 | const dev = process.env.NODE_ENV !== "production";
9 | const port = process.env.PORT || 3000;
10 |
11 | const app = next({ dev });
12 |
13 | const handle = app.getRequestHandler();
14 |
15 | const readStorybookStatic = (filename) => {
16 | const resolvedBase = path.resolve("./storybook-static");
17 | const fileLoc = path.join(resolvedBase, filename);
18 | return fs.readFile(fileLoc);
19 | };
20 |
21 | app.prepare().then(() => {
22 | const httpServer = createServer(async (req, res) => {
23 | // Be sure to pass `true` as the second argument to `url.parse`.
24 | // This tells it to parse the query portion of the URL.
25 | const parsedUrl = parse(req.url, true);
26 | const { pathname } = parsedUrl;
27 |
28 | if (pathname.startsWith("/storybook")) {
29 | if (dev) {
30 | res.statusCode = 400;
31 | return res.end(
32 | "Forbidden: Please run `npm run storybook` in development"
33 | );
34 | }
35 | if (pathname === "/storybook") {
36 | res.statusCode = 302;
37 | res.setHeader("Location", "/storybook/index.html");
38 | return res.end();
39 | }
40 |
41 | const filename = pathname.split("/storybook")[1];
42 | const file = await readStorybookStatic(filename);
43 | res.statusCode = 200;
44 | res.write(file);
45 | return res.end();
46 | }
47 | // Forward to next handler
48 | handle(req, res, parsedUrl);
49 | });
50 |
51 | listenSocket(httpServer);
52 |
53 | httpServer.listen(port, () => {
54 | console.log(`> Ready on http://localhost:${port}`);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fullstack-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "nodemon",
7 | "build": "npm run build-next && npm run build-server",
8 | "start": "NODE_ENV=production node build/index.js",
9 | "lint": "eslint .",
10 | "stylelint": "stylelint '**/*.css'",
11 | "prettify": "prettier --check .",
12 | "prepare": "husky install",
13 | "typecheck": "tsc",
14 | "test": "npm run lint && npm run stylelint && npm run prettify && npm run typecheck",
15 | "storybook": "start-storybook -s ./public -p 6006",
16 | "build-next": "next build",
17 | "build-storybook": "build-storybook -s public",
18 | "build-server": "tsc --project tsconfig.server.json"
19 | },
20 | "dependencies": {
21 | "next": "10.0.8",
22 | "react": "17.0.1",
23 | "react-dom": "17.0.1",
24 | "socket.io": "^4.0.0",
25 | "socket.io-client": "^4.0.0"
26 | },
27 | "devDependencies": {
28 | "@babel/core": "^7.13.10",
29 | "@storybook/addon-actions": "^6.1.21",
30 | "@storybook/addon-essentials": "^6.1.21",
31 | "@storybook/addon-links": "^6.1.21",
32 | "@storybook/react": "^6.1.21",
33 | "@types/node": "^14.14.33",
34 | "@types/react": "^17.0.3",
35 | "@typescript-eslint/eslint-plugin": "^4.17.0",
36 | "@typescript-eslint/parser": "^4.17.0",
37 | "babel-loader": "^8.2.2",
38 | "eslint": "^7.21.0",
39 | "eslint-config-prettier": "^8.1.0",
40 | "eslint-plugin-react": "^7.22.0",
41 | "husky": "^5.1.3",
42 | "lint-staged": "^10.5.4",
43 | "nodemon": "^2.0.7",
44 | "prettier": "^2.2.1",
45 | "storybook-css-modules-preset": "^1.0.6",
46 | "stylelint": "^13.12.0",
47 | "stylelint-config-prettier": "^8.0.2",
48 | "stylelint-config-standard": "^21.0.0",
49 | "ts-node": "^9.1.1",
50 | "typescript": "^4.2.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pages/user.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import styles from "../styles/User.module.css";
3 | import Link from "next/link";
4 | import Logo from "../components/logo/Logo";
5 | import BackBtn from "../components/button/BackBtn";
6 | import ConfirmBtn from "../components/button/ConfirmBtn";
7 | import NameInput from "../components/input/NameInput";
8 | import { useRouter } from "next/dist/client/router";
9 | import { useState } from "react";
10 |
11 | export default function User() {
12 | const [isMaxLength, setIsMaxLength] = useState(false);
13 | const [playerName, setPlayerName] = useState("");
14 | const router = useRouter();
15 |
16 | const handleSubmit = (event) => {
17 | event.preventDefault();
18 | localStorage.setItem("playerName", playerName);
19 | router.push("/lobbies");
20 | };
21 |
22 | const checkValueLength = (event) => {
23 | setIsMaxLength(event.target.value.length >= 11);
24 | };
25 |
26 | const onHandleChange = (event) => {
27 | checkValueLength(event);
28 | setPlayerName(event.target.value);
29 | };
30 |
31 | return (
32 | <>
33 |
34 | Username
35 |
36 |
37 |
38 |
39 |
57 |
58 | >
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/pages/rules.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import BackBtn from "../components/button/BackBtn";
3 | import Logo from "../components/logo/Logo";
4 | import styles from "../styles/Rules.module.css";
5 | import Link from "next/link";
6 |
7 | export default function Rules() {
8 | return (
9 | <>
10 |
11 | How to play
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Every player has 12 hidden cards (arranged in a 3x4 cardgrid).
27 | At the start of the game two cards are turned face up, revealing
28 | the card.
29 | The player whose two cards have the highest sum (score) goes
30 | first.
31 | On your turn you can take the top card from the discard or draw
32 | pile. You can exchange one card (hidden or open) from your
33 | cardgrid.
34 | Round ends when one player has only open/revealed cards on his
35 | cardgrid.
36 | When the round ends, every player has one more turn (except the
37 | player, who ends the round).
38 | Afterwards all non-revealed cards will be revealed. When all
39 | cards are revealed the numbers of the cards each player has will
40 | be added for scoring.
41 | Game ends after one player has 100 or more points. Whoever has
42 | the lowest number wins.
43 |
44 |
45 | Special rule: Whenever one row of 3 cards all have the same
46 | value, they will be discarded and no longer scored.
47 | Cards are ranked from -2 up to 12.
48 |
49 |
50 |
51 |
52 |
53 | >
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Roboto";
3 | src: url("../public/fonts/Roboto-Black.ttf") format("truetype");
4 | font-weight: 900;
5 | }
6 |
7 | @font-face {
8 | font-family: "Roboto";
9 | src: url("../public/fonts/Roboto-Bold.ttf") format("truetype");
10 | font-weight: 700;
11 | }
12 |
13 | @font-face {
14 | font-family: "Roboto";
15 | src: url("../public/fonts/Roboto-Medium.ttf") format("truetype");
16 | font-weight: 500;
17 | }
18 |
19 | @font-face {
20 | font-family: "Roboto";
21 | src: url("../public/fonts/Roboto-Regular.ttf") format("truetype");
22 | font-weight: 400;
23 | }
24 |
25 | @font-face {
26 | font-family: "Roboto";
27 | src: url("../public/fonts/Roboto-Light.ttf") format("truetype");
28 | font-weight: 300;
29 | }
30 |
31 | @font-face {
32 | font-family: "Roboto";
33 | src: url("../public/fonts/Roboto-Thin.ttf") format("truetype");
34 | font-weight: 100;
35 | }
36 |
37 | :root {
38 | --primary-color: #141225;
39 | --secondary-color: #ffce21;
40 | --text-color: #fff;
41 | --placeholder-color: #8a8a8a;
42 | --background-gradient-color: #62606d;
43 | --bottom-transition: background-size 250ms ease-out;
44 | --shadow: 0 3px 10px #000000c7;
45 | }
46 |
47 | html,
48 | body {
49 | padding: 0;
50 | margin: 0;
51 | font-family: "Roboto", sans-serif;
52 | font-size: 16px;
53 | color: var(--text-color);
54 | background: transparent
55 | radial-gradient(
56 | closest-side at 50% 50%,
57 | var(--primary-color) 0%,
58 | var(--background-gradient-color) 400%
59 | )
60 | 0% 0% no-repeat padding-box;
61 | }
62 |
63 | button {
64 | font-family: inherit;
65 | font-weight: 500;
66 | color: var(--text-color);
67 | background: none;
68 | border: none;
69 | }
70 |
71 | a {
72 | color: inherit;
73 | text-decoration: none;
74 | }
75 |
76 | h2 {
77 | margin: 0;
78 | font-size: 2.7em;
79 | font-weight: bold;
80 | padding: 0.05em 0.2em;
81 | line-height: 1em;
82 | background-image: linear-gradient(
83 | var(--secondary-color),
84 | var(--secondary-color)
85 | ),
86 | linear-gradient(var(--secondary-color), var(--secondary-color));
87 | background-size: 3px 58%, 52% 3px;
88 | background-repeat: no-repeat;
89 | background-position: right bottom, right bottom;
90 | }
91 |
92 | * {
93 | box-sizing: border-box;
94 | }
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SISU · [](https://travis-ci.org/npm/npm) [](https://www.npmjs.com/package/npm) [](http://makeapullrequest.com) [](https://github.com/your/your-project/blob/master/LICENSE)
2 |
3 | > A multiplayer card game for up to 8 players
4 |
5 | This is my capstone project for the Neue Fische Web Development bootcamp I attended in spring of 2021.
6 | It's a web app multiplayer card game for up to 8 players, best played in a web browser.
7 | Reveal your cards and exchange them cleverly to reach the lowest score each round and beat your friends.
8 | The rules of this game are based on the fantastic card game **Skyjo**
9 |
10 | ## Installing / Getting started
11 |
12 | To run locally, install the dependencies and run the development server.
13 |
14 | ```shell
15 | npm install
16 | npm run dev
17 | ```
18 |
19 | Now you should be able to run the game on port 3000.
20 |
21 | ## Developing
22 |
23 | ### Built With
24 |
25 | - React.js
26 | - Next.js
27 | - Socket.io
28 | - TypeScript
29 |
30 | ### Prerequisites
31 |
32 | None needed, just install the dependencies with `npm install`
33 |
34 | ### Setting up Dev
35 |
36 | If you want to add features, feel free to do so 🧙♂️
37 |
38 | ```shell
39 | git clone git@github.com:fre-ben/sisu.git
40 | cd sisu
41 | npm install
42 | npm run dev
43 | ```
44 |
45 | ## Tests
46 |
47 | No tests implemented yet.
48 |
49 | ## Project Dependencies
50 |
51 | - https://nextjs.org/learn/excel/typescript
52 | - https://prettier.io/docs/en/install.html
53 | - https://eslint.org/docs/user-guide/getting-started
54 | - https://github.com/prettier/eslint-config-prettier
55 | - https://stylelint.io/user-guide/get-started
56 | - https://github.com/prettier/stylelint-config-prettier
57 | - https://typicode.github.io/husky/
58 | - https://github.com/okonet/lint-staged
59 | - https://storybook.js.org/docs/react/get-started/install
60 | - https://www.npmjs.com/package/storybook-css-modules-preset
61 | - [Custom server for next](https://nextjs.org/docs/advanced-features/custom-server)
62 | - [TypeScript for custom server + nodemon](https://github.com/vercel/next.js/tree/canary/examples/custom-server-typescript)
63 | - [socket.io](https://socket.io/)
64 |
--------------------------------------------------------------------------------
/components/button/Button.module.css:
--------------------------------------------------------------------------------
1 | .btn {
2 | font-size: 2.7em;
3 | padding: 0.05em 0.2em;
4 | line-height: 1em;
5 | background-image: linear-gradient(
6 | var(--secondary-color),
7 | var(--secondary-color)
8 | ),
9 | linear-gradient(var(--secondary-color), var(--secondary-color));
10 | background-size: 3px 58%, var(--bottom-border-length) 3px;
11 | background-repeat: no-repeat;
12 | background-position: right bottom, right bottom;
13 | cursor: pointer;
14 | transition: var(--bottom-transition);
15 | }
16 |
17 | .btn:disabled {
18 | background-image: linear-gradient(
19 | var(--placeholder-color),
20 | var(--placeholder-color)
21 | ),
22 | linear-gradient(var(--placeholder-color), var(--placeholder-color));
23 | color: var(--placeholder-color);
24 | cursor: default;
25 | }
26 |
27 | .btn:hover {
28 | background-size: 3px 58%, calc(var(--bottom-border-length) + 10%) 3px;
29 | }
30 |
31 | .startBtn {
32 | --bottom-border-length: 38%;
33 | }
34 |
35 | .rulesBtn {
36 | --bottom-border-length: 25%;
37 |
38 | padding: 0.25em 0.2em;
39 | }
40 |
41 | .confirmBtn {
42 | --bottom-border-length: 77%;
43 | }
44 |
45 | .backContainer {
46 | display: grid;
47 | grid-auto-flow: column;
48 | align-items: center;
49 | column-gap: 0.2em;
50 | }
51 |
52 | .backBtn {
53 | --bottom-border-length: 51%;
54 |
55 | padding: 0.05em 0.22em;
56 | }
57 |
58 | .joinBtn {
59 | --bottom-border-length: 62%;
60 |
61 | font-size: 2.2em;
62 | padding: 0.25em 0.22em;
63 | }
64 |
65 | .btnBackground {
66 | background-color: var(--primary-color);
67 | padding: 0.2em 0.4em;
68 | box-shadow: var(--shadow);
69 | border-radius: 5px;
70 | }
71 |
72 | .btnBackground span {
73 | color: var(--secondary-color);
74 | }
75 |
76 | .gameBoardBtn {
77 | font-size: 1.8em;
78 | padding: 0.3em 0.27em;
79 | }
80 |
81 | .joinBackground {
82 | padding: 0.05em 0.23em;
83 | }
84 |
85 | .createBackground {
86 | padding: 0.2em 0.2em;
87 | }
88 |
89 | .createBtn {
90 | --bottom-border-length: 37%;
91 |
92 | font-size: 1.7em;
93 | padding: 0.3em 0.25em;
94 | }
95 |
96 | .keepBtn {
97 | --bottom-border-length: 62%;
98 | }
99 |
100 | .discardBtn {
101 | --bottom-border-length: 70.5%;
102 | }
103 |
104 | .readyBtn {
105 | --bottom-border-length: 26%;
106 | }
107 |
108 | .restartBtn {
109 | --bottom-border-length: 23%;
110 | }
111 |
112 | .exitBtn {
113 | --bottom-border-length: 41%;
114 |
115 | font-size: 1.8em;
116 | padding: 0.15em 0.27em;
117 | }
118 |
119 | .continueBtn {
120 | --bottom-border-length: 75%;
121 |
122 | font-size: 1.8em;
123 | padding: 0.32em 0.25em;
124 | }
125 |
126 | .continueBackground {
127 | padding: 0.25em 0.3em;
128 | }
129 |
--------------------------------------------------------------------------------
/components/gameboard/CardGrid.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 | grid-template-areas:
5 | "score name"
6 | "cardgrid cardgrid";
7 | }
8 |
9 | .container p {
10 | margin: 0;
11 | margin-bottom: 0.2em;
12 | font-size: 1.6em;
13 | font-weight: 400;
14 | padding: 0 1em;
15 | }
16 |
17 | .container p span {
18 | color: var(--secondary-color);
19 | }
20 |
21 | .opponentContainer p {
22 | margin: 0;
23 | margin-top: 0.5em;
24 | font-size: 1em;
25 | font-weight: 300;
26 | padding: 0 1em;
27 | }
28 |
29 | .container p:first-of-type {
30 | grid-area: score;
31 | }
32 |
33 | .container p:last-of-type {
34 | grid-area: name;
35 | justify-self: end;
36 | }
37 |
38 | .lines {
39 | grid-area: cardgrid;
40 | padding: 0.7em;
41 | background-image: linear-gradient(
42 | var(--secondary-color),
43 | var(--secondary-color)
44 | ),
45 | linear-gradient(var(--secondary-color), var(--secondary-color)),
46 | linear-gradient(var(--secondary-color), var(--secondary-color)),
47 | linear-gradient(var(--secondary-color), var(--secondary-color));
48 | background-size: 3px 30%, 20% 3px;
49 | background-repeat: no-repeat;
50 | background-position: right bottom, right bottom, left top, left top;
51 | transition: var(--bottom-transition);
52 | }
53 |
54 | .lines:hover {
55 | background-size: 3px 30%, 30% 3px;
56 | }
57 |
58 | .cardGrid {
59 | display: grid;
60 | grid-template-columns: repeat(4, 1fr);
61 | grid-template-rows: repeat(3, 1fr);
62 | column-gap: 1em;
63 | row-gap: 0.5em;
64 | background-color: var(--primary-color);
65 | padding: 1.3em;
66 | box-shadow: var(--shadow);
67 | border-radius: 5px;
68 | min-width: 28em;
69 | }
70 |
71 | .card {
72 | width: 5.8em;
73 | cursor: pointer;
74 | }
75 |
76 | .notClickable {
77 | cursor: default;
78 | }
79 |
80 | .opponentContainer {
81 | display: grid;
82 | grid-template-columns: 1fr 1fr;
83 | grid-template-areas:
84 | "cardgrid cardgrid"
85 | "score name";
86 | }
87 |
88 | .opponentContainer p span {
89 | color: var(--secondary-color);
90 | }
91 |
92 | .opponentContainer p:first-of-type {
93 | grid-area: score;
94 | }
95 |
96 | .opponentContainer p:last-of-type {
97 | grid-area: name;
98 | justify-self: end;
99 | }
100 |
101 | .oppCardGrid {
102 | display: grid;
103 | grid-area: cardgrid;
104 | grid-template-columns: repeat(4, 1fr);
105 | grid-template-rows: repeat(3, 1fr);
106 | column-gap: 0.5em;
107 | row-gap: 0.3em;
108 | background-color: var(--primary-color);
109 | padding: 0.6em;
110 | box-shadow: var(--shadow);
111 | border-radius: 5px;
112 | min-width: 180px;
113 | }
114 |
115 | .oppCardGrid img {
116 | width: 2.1em;
117 | cursor: default;
118 | }
119 |
--------------------------------------------------------------------------------
/components/gameboard/DrawPilePrompt.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import { SocketContext } from "../../contexts/SocketContext";
3 | import { getLobbyNr } from "../../lib/functions";
4 | import { Card } from "../../server/lib/gameTypes";
5 | import { phase } from "../../server/lib/turnPhases";
6 | import DiscardBtn from "../button/DiscardBtn";
7 | import KeepBtn from "../button/KeepBtn";
8 | import styles from "./DrawPilePrompt.module.css";
9 |
10 | type DrawPilePrompt = {
11 | turnPhase: string;
12 | };
13 |
14 | function DrawPilePrompt({ turnPhase }: DrawPilePrompt) {
15 | const { socket } = useContext(SocketContext);
16 | const lobbyNr = getLobbyNr();
17 | const [drawPileCard, setDrawPileCard] = useState(null);
18 |
19 | useEffect(() => {
20 | if (!socket || !lobbyNr) {
21 | return;
22 | }
23 |
24 | switch (turnPhase) {
25 | case phase.DRAWPILEDECISION:
26 | socket.emit("get new drawpilecard", lobbyNr);
27 | break;
28 | default:
29 | socket.emit("get current drawpilecard", lobbyNr);
30 | break;
31 | }
32 |
33 | function handleDisplayDrawPileCard(card: Card) {
34 | setDrawPileCard(card);
35 | }
36 |
37 | socket.on("display new drawpilecard", (card) => {
38 | if (!card) {
39 | socket.emit("get new drawpilecard", lobbyNr);
40 | } else {
41 | handleDisplayDrawPileCard(card);
42 | }
43 | });
44 |
45 | socket.on("display current drawpilecard", (card) => {
46 | handleDisplayDrawPileCard(card);
47 | });
48 |
49 | return () => {
50 | socket.off("display new drawpilecard");
51 | socket.off("display current drawpilecard");
52 | };
53 | }, [socket, lobbyNr]);
54 |
55 | function handleKeepClick() {
56 | switch (turnPhase) {
57 | case phase.DRAWPILEDECISION:
58 | socket.emit("DRAWPILEDECISION: click keep", socket.id, lobbyNr);
59 | break;
60 | case phase.WAITTURN:
61 | return;
62 | default:
63 | return;
64 | }
65 | }
66 |
67 | function handleDiscardClick() {
68 | switch (turnPhase) {
69 | case phase.DRAWPILEDECISION:
70 | socket.emit("DRAWPILEDECISION: click discard", socket.id, lobbyNr);
71 | break;
72 | case phase.WAITTURN:
73 | return;
74 | default:
75 | return;
76 | }
77 | }
78 |
79 | return (
80 |
81 | {turnPhase === phase.DRAWPILEDECISION && (
82 |
83 |
84 |
85 |
86 | )}
87 |
91 |
92 | );
93 | }
94 |
95 | export default DrawPilePrompt;
96 |
--------------------------------------------------------------------------------
/pages/lobbies.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import styles from "../styles/Lobbies.module.css";
3 | import Link from "next/link";
4 | import Logo from "../components/logo/Logo";
5 | import BackBtn from "../components/button/BackBtn";
6 | import CreateBtn from "../components/button/CreateBtn";
7 | import LobbyListItem from "../components/misc/LobbyListItem";
8 | import { useRouter } from "next/router";
9 | import { useContext, useEffect, useState } from "react";
10 | import { SocketContext } from "../contexts/SocketContext";
11 | import { getPlayerName } from "../lib/functions";
12 | import { GameForLobby } from "../server/lib/gameTypes";
13 |
14 | export default function Lobbies() {
15 | const { socket } = useContext(SocketContext);
16 | const [lobbyItems, setLobbyItems] = useState([]);
17 | const router = useRouter();
18 |
19 | useEffect(() => {
20 | if (!socket) {
21 | return;
22 | }
23 | function handleDisplayGames(games: GameForLobby[]) {
24 | setLobbyItems(games);
25 | }
26 |
27 | socket.on("display list of games", handleDisplayGames);
28 | socket.emit("get list of games");
29 | }, [socket]);
30 |
31 | const goToLobby = (lobbyNr: number) => {
32 | router.push(`/game?lobby=${lobbyNr}`);
33 | };
34 |
35 | const handleJoinBtnClick = (lobbyNr: number): void => {
36 | const playerName = getPlayerName();
37 | socket.emit("join game", lobbyNr, playerName, socket.id);
38 | goToLobby(lobbyNr);
39 | };
40 |
41 | const handleCreateBtnClick = (): void => {
42 | const playerName = getPlayerName();
43 | socket.emit("create game", playerName, socket.id);
44 | socket.on("pass lobbynr", (lobbyNr: number) => {
45 | goToLobby(lobbyNr);
46 | });
47 | };
48 |
49 | return (
50 | <>
51 |
52 | Lobbies
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | {lobbyItems.map(
68 | ({ lobbyNr, playerCount, lobbyIsFull, hasStarted }) => {
69 | return (
70 | handleJoinBtnClick(lobbyNr)}
75 | lobbyIsFull={lobbyIsFull}
76 | hasStarted={hasStarted}
77 | />
78 | );
79 | }
80 | )}
81 |
82 |
83 |
84 |
85 | >
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/server/lib/cards.ts:
--------------------------------------------------------------------------------
1 | import { Card, CardToGenerate } from "./gameTypes";
2 |
3 | export function generateCards(): Card[] {
4 | const cardsToGenerate: CardToGenerate[] = [
5 | [
6 | {
7 | value: -2,
8 | imgSrc: "/cards/m2.png",
9 | hidden: true,
10 | },
11 | 5,
12 | ],
13 | [
14 | {
15 | value: -1,
16 | imgSrc: "/cards/m1.png",
17 | hidden: true,
18 | },
19 | 10,
20 | ],
21 | [
22 | {
23 | value: 0,
24 | imgSrc: "/cards/00.png",
25 | hidden: true,
26 | },
27 | 15,
28 | ],
29 | [
30 | {
31 | value: 1,
32 | imgSrc: "/cards/01.png",
33 | hidden: true,
34 | },
35 | 10,
36 | ],
37 | [
38 | {
39 | value: 2,
40 | imgSrc: "/cards/02.png",
41 | hidden: true,
42 | },
43 | 10,
44 | ],
45 | [
46 | {
47 | value: 3,
48 | imgSrc: "/cards/03.png",
49 | hidden: true,
50 | },
51 | 10,
52 | ],
53 | [
54 | {
55 | value: 4,
56 | imgSrc: "/cards/04.png",
57 | hidden: true,
58 | },
59 | 10,
60 | ],
61 | [
62 | {
63 | value: 5,
64 | imgSrc: "/cards/05.png",
65 | hidden: true,
66 | },
67 | 10,
68 | ],
69 | [
70 | {
71 | value: 6,
72 | imgSrc: "/cards/06.png",
73 | hidden: true,
74 | },
75 | 10,
76 | ],
77 | [
78 | {
79 | value: 7,
80 | imgSrc: "/cards/07.png",
81 | hidden: true,
82 | },
83 | 10,
84 | ],
85 | [
86 | {
87 | value: 8,
88 | imgSrc: "/cards/08.png",
89 | hidden: true,
90 | },
91 | 10,
92 | ],
93 | [
94 | {
95 | value: 9,
96 | imgSrc: "/cards/09.png",
97 | hidden: true,
98 | },
99 | 10,
100 | ],
101 | [
102 | {
103 | value: 10,
104 | imgSrc: "/cards/10.png",
105 | hidden: true,
106 | },
107 | 10,
108 | ],
109 | [
110 | {
111 | value: 11,
112 | imgSrc: "/cards/11.png",
113 | hidden: true,
114 | },
115 | 10,
116 | ],
117 | [
118 | {
119 | value: 12,
120 | imgSrc: "/cards/12.png",
121 | hidden: true,
122 | },
123 | 10,
124 | ],
125 | ];
126 |
127 | const cards: Card[] = [];
128 |
129 | cardsToGenerate.forEach(([value, quantity]) => {
130 | cards.push(...Array(quantity).fill(value));
131 | });
132 |
133 | return cards.map((card, index) => {
134 | return {
135 | id: index + 1,
136 | value: card.value,
137 | imgSrc: card.imgSrc,
138 | hidden: card.hidden,
139 | };
140 | });
141 | }
142 |
143 | export function generateBlankCards(): Card[] {
144 | const cardsToGenerate: CardToGenerate[] = [
145 | [
146 | {
147 | value: 0,
148 | imgSrc: "/cards/blank.png",
149 | hidden: false,
150 | },
151 | 12,
152 | ],
153 | ];
154 |
155 | const blankCards: Card[] = [];
156 | cardsToGenerate.forEach(([value, quantity]) => {
157 | blankCards.push(...Array(quantity).fill(value));
158 | });
159 |
160 | return blankCards;
161 | }
162 |
--------------------------------------------------------------------------------
/components/gameboard/CardGrid.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 | import { SocketContext } from "../../contexts/SocketContext";
3 | import { getLobbyNr } from "../../lib/functions";
4 | import { Card } from "../../server/lib/gameTypes";
5 | import { phase } from "../../server/lib/turnPhases";
6 | import styles from "./CardGrid.module.css";
7 | import type { PlayerCardGridProps } from "./OpponentCardGrid";
8 |
9 | function CardGrid({
10 | cards,
11 | name,
12 | roundScore,
13 | gameHasStarted,
14 | turnPhase,
15 | }: PlayerCardGridProps) {
16 | const { socket } = useContext(SocketContext);
17 | const [roundStart, setRoundStart] = useState(false);
18 | const lobbyNr = getLobbyNr();
19 |
20 | const notClickable = `${styles.card} ${styles.notClickable}`;
21 |
22 | const handleCardClick = (index: number, card: Card): void => {
23 | if (gameHasStarted && !roundStart) {
24 | socket.emit("cardgrid click", socket.id, lobbyNr, index);
25 | socket.emit(
26 | "check 2 cards revealed",
27 | socket.id,
28 | lobbyNr,
29 | (bothCardsRevealed) => {
30 | if (bothCardsRevealed) {
31 | setRoundStart(true);
32 | }
33 | }
34 | );
35 | }
36 | if (roundStart) {
37 | switch (turnPhase) {
38 | case phase.DRAWDECISION:
39 | return;
40 | case phase.DISCARDPILEDECISION:
41 | socket.emit("DISCARDPILE: replace card", socket.id, lobbyNr, index);
42 | break;
43 | case phase.DRAWPILEKEEP:
44 | socket.emit("DRAWPILE: replace card", socket.id, lobbyNr, index);
45 | break;
46 | case phase.DRAWPILEDISCARD:
47 | if (card.hidden === false) {
48 | socket.emit("DRAWPILE: invalid reveal card", socket.id, lobbyNr);
49 | return;
50 | }
51 | socket.emit("DRAWPILE: reveal card", socket.id, lobbyNr, index);
52 | break;
53 | case phase.WAITTURN:
54 | return;
55 | default:
56 | return;
57 | }
58 | }
59 | };
60 |
61 | function cardStyle() {
62 | switch (turnPhase) {
63 | case phase.DRAWDECISION:
64 | return notClickable;
65 | case phase.WAITTURN:
66 | return notClickable;
67 | case phase.DRAWPILEDECISION:
68 | return notClickable;
69 | case "discardPileDecision":
70 | return styles.card;
71 | default:
72 | return styles.card;
73 | }
74 | }
75 |
76 | const createPlayerCardGrid = cards.map((card, index) => (
77 | handleCardClick(index, card)
86 | : () => {
87 | return;
88 | }
89 | }
90 | />
91 | ));
92 |
93 | return (
94 |
95 |
96 | Score: {roundScore}
97 |
98 | {name}
99 |
100 |
{createPlayerCardGrid}
101 |
102 |
103 | );
104 | }
105 |
106 | export default CardGrid;
107 |
--------------------------------------------------------------------------------
/server/lib/broadcasts.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getActivePlayer,
3 | getCurrentDrawPileCard,
4 | getDiscardPile,
5 | getFirstActivePlayer,
6 | getGameByLobby,
7 | getGamesForLobby,
8 | getNewDrawPileCard,
9 | getPlayerCount,
10 | getPlayersInLobby,
11 | getRoundNr,
12 | getTotalScores,
13 | } from "./games";
14 | import { status } from "./statusMessages";
15 | import { phase } from "./turnPhases";
16 |
17 | export function broadcastListGamesUpdate(io): void {
18 | io.emit("display list of games", getGamesForLobby());
19 | }
20 |
21 | export function broadcastPlayerCountToLobby(io, lobbyNr: number): void {
22 | io.to(`lobby${lobbyNr}`).emit(
23 | "display current playercount",
24 | getPlayerCount(lobbyNr)
25 | );
26 | }
27 |
28 | export function broadcastTotalScoresToLobby(io, lobbyNr: number): void {
29 | io.to(`lobby${lobbyNr}`).emit("display scores", getTotalScores(lobbyNr));
30 | }
31 |
32 | export function broadcastPlayersToLobby(io, lobbyNr: number): void {
33 | io.to(`lobby${lobbyNr}`).emit("display players", getPlayersInLobby(lobbyNr));
34 | }
35 |
36 | export function broadcastGameStartToLobby(io, lobbyNr: number): void {
37 | io.to(`lobby${lobbyNr}`).emit("all players ready", getGameByLobby(lobbyNr));
38 | }
39 |
40 | export function broadcastDiscardPileToLobby(io, lobbyNr: number): void {
41 | io.to(`lobby${lobbyNr}`).emit("display discardpile", getDiscardPile(lobbyNr));
42 | }
43 |
44 | export function broadcastRoundNrToLobby(io, lobbyNr: number): void {
45 | io.to(`lobby${lobbyNr}`).emit("display rounds", getRoundNr(lobbyNr));
46 | }
47 |
48 | export function broadcastNewDrawPileCardToLobby(io, lobbyNr: number): void {
49 | io.to(`lobby${lobbyNr}`).emit(
50 | "display new drawpilecard",
51 | getNewDrawPileCard(lobbyNr)
52 | );
53 | }
54 |
55 | export function broadcastCurrentDrawPileCardToLobby(io, lobbyNr: number): void {
56 | io.to(`lobby${lobbyNr}`).emit(
57 | "display current drawpilecard",
58 | getCurrentDrawPileCard(lobbyNr)
59 | );
60 | }
61 |
62 | export async function broadcastFirstActivePlayerToLobby(
63 | io,
64 | lobbyNr: number,
65 | socketID: string
66 | ): Promise {
67 | io.to(`lobby${lobbyNr}`).emit(
68 | "set first active player",
69 | await getFirstActivePlayer(lobbyNr, socketID)
70 | );
71 | }
72 |
73 | export async function broadcastStatusToActivePlayer(
74 | io,
75 | socketID: string,
76 | lobbyNr: number,
77 | activePlayerStatus: string
78 | ) {
79 | const activePlayer = await getActivePlayer(socketID);
80 | io.to(`lobby${lobbyNr}`).emit(
81 | "display status",
82 | status.WAITTURN(activePlayer.name)
83 | );
84 | io.to(activePlayer.socketID).emit("display status", activePlayerStatus);
85 | }
86 |
87 | export async function broadcastTurnPhaseToActivePlayer(
88 | io,
89 | socketID: string,
90 | lobbyNr: number,
91 | activePlayerTurnPhase: string
92 | ) {
93 | const activePlayer = await getActivePlayer(socketID);
94 | io.to(`lobby${lobbyNr}`).emit("set turn phase", phase.WAITTURN);
95 | io.to(activePlayer.socketID).emit("set turn phase", activePlayerTurnPhase);
96 | }
97 |
98 | export async function broadcastTurnStartToActivePlayer(
99 | io,
100 | socketID: string,
101 | lobbyNr: number
102 | ) {
103 | await broadcastStatusToActivePlayer(
104 | io,
105 | socketID,
106 | lobbyNr,
107 | status.DRAWDECISION
108 | );
109 | await broadcastTurnPhaseToActivePlayer(
110 | io,
111 | socketID,
112 | lobbyNr,
113 | phase.DRAWDECISION
114 | );
115 | }
116 |
--------------------------------------------------------------------------------
/pages/game.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import styles from "../styles/Game.module.css";
3 | import Logo from "../components/logo/Logo";
4 | import ExitBtn from "../components/button/ExitBtn";
5 | import RoundCounter from "../components/gameboard/RoundCounter";
6 | import Statusbar from "../components/gameboard/Statusbar";
7 | import ReadyBtn from "../components/button/ReadyBtn";
8 | import TotalScore from "../components/gameboard/TotalScore";
9 | import DrawPile from "../components/gameboard/DrawPile";
10 | import DiscardPile from "../components/gameboard/DiscardPile";
11 | import CardGrid from "../components/gameboard/CardGrid";
12 | import OpponentCardGrid from "../components/gameboard/OpponentCardGrid";
13 | import { SocketContext } from "../contexts/SocketContext";
14 | import { useContext, useEffect, useState } from "react";
15 | import { getLobbyNr } from "../lib/functions";
16 | import { useRouter } from "next/router";
17 | import type {
18 | ActivePlayer,
19 | Card,
20 | PlayerForCardGrid,
21 | } from "../server/lib/gameTypes";
22 | import { generateBlankCards } from "../server/lib/cards";
23 | import DrawPilePrompt from "../components/gameboard/DrawPilePrompt";
24 |
25 | export default function Game() {
26 | const { socket } = useContext(SocketContext);
27 | const [playerCount, setPlayerCount] = useState(null);
28 | const [opponentPlayers, setOpponentPlayers] = useState(
29 | []
30 | );
31 | const [player, setPlayer] = useState(null);
32 | const [discardPileCard, setDiscardPileCard] = useState(null);
33 | const [gameHasStarted, setGameHasStarted] = useState(false);
34 | const [activePlayer, setActivePlayer] = useState(null);
35 | const [turnPhase, setTurnPhases] = useState(null);
36 | const router = useRouter();
37 | const lobbyNr = getLobbyNr();
38 | const blankCards = generateBlankCards();
39 |
40 | useEffect(() => {
41 | if (!socket || !lobbyNr) {
42 | return;
43 | }
44 |
45 | function handleCurrentPlayerCount(count: number) {
46 | setPlayerCount(count);
47 | }
48 |
49 | function handleDisplayPlayers(players: PlayerForCardGrid[]) {
50 | const opponentPlayers = players.filter(
51 | (player) => player.socketID !== socket.id
52 | );
53 | const player = players.find((player) => player.socketID === socket.id);
54 | setOpponentPlayers(opponentPlayers);
55 | setPlayer(player);
56 | }
57 |
58 | function handleDisplayDiscardPile(card: Card) {
59 | setDiscardPileCard(card);
60 | }
61 |
62 | socket.emit("get discardpile", lobbyNr);
63 | socket.on("display discardpile", handleDisplayDiscardPile);
64 |
65 | socket.emit("get playercount", lobbyNr);
66 | socket.on("display current playercount", handleCurrentPlayerCount);
67 |
68 | socket.emit("get players", lobbyNr, socket.id);
69 | socket.on("display players", (players) => {
70 | if (!players) {
71 | socket.emit("get players", lobbyNr, socket.id);
72 | } else {
73 | handleDisplayPlayers(players);
74 | }
75 | });
76 |
77 | socket.on("set first active player", setActivePlayer);
78 |
79 | socket.on("set turn phase", setTurnPhases);
80 |
81 | return () => {
82 | socket.off("display discardpile", handleDisplayDiscardPile);
83 | socket.off("display current playercount", handleCurrentPlayerCount);
84 | socket.off("display players", handleDisplayPlayers);
85 | socket.off("set first active player", setActivePlayer);
86 | };
87 | }, [socket, lobbyNr]);
88 |
89 | const handleExitBtnClick = (): void => {
90 | if (
91 | confirm(
92 | "Do you really want to leave the game? (Reconnecting is not possible)"
93 | )
94 | ) {
95 | socket.emit("leave game", socket.id, lobbyNr);
96 | router.push("/lobbies");
97 | }
98 | };
99 |
100 | const handleReadyBtnClick = (): void => {
101 | socket.emit("player is ready", socket.id, lobbyNr);
102 | socket.emit("check all players ready", lobbyNr);
103 | socket.on("all players ready", (game) => {
104 | setGameHasStarted(game.hasStarted);
105 | });
106 | };
107 |
108 | const opponentCardGrids = opponentPlayers.map(
109 | ({ name, cards, roundScore, socketID }) => {
110 | return (
111 |
117 | );
118 | }
119 | );
120 |
121 | const opponentsLayout = {
122 | /* stylelint-disable */
123 | gridTemplateColumns: `repeat(${Math.min(6, playerCount - 1)}, 1fr)`,
124 | /* stylelint-enable */
125 | };
126 |
127 | return (
128 | <>
129 |
130 | Sisu - Lobby #{lobbyNr}
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | {playerCount >= 2 && player && !player.isReady && (
142 |
143 | )}
144 |
145 |
150 |
151 |
152 | {opponentCardGrids}
153 |
154 |
161 | {player && (
162 |
169 | )}
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | >
178 | );
179 | }
180 |
--------------------------------------------------------------------------------
/server/socket.ts:
--------------------------------------------------------------------------------
1 | import { Server, Socket } from "socket.io";
2 | import { phase } from "./lib/turnPhases";
3 | import {
4 | broadcastCurrentDrawPileCardToLobby,
5 | broadcastDiscardPileToLobby,
6 | broadcastFirstActivePlayerToLobby,
7 | broadcastGameStartToLobby,
8 | broadcastListGamesUpdate,
9 | broadcastNewDrawPileCardToLobby,
10 | broadcastPlayerCountToLobby,
11 | broadcastPlayersToLobby,
12 | broadcastStatusToActivePlayer,
13 | broadcastTotalScoresToLobby,
14 | broadcastTurnPhaseToActivePlayer,
15 | broadcastTurnStartToActivePlayer,
16 | broadcastRoundNrToLobby,
17 | } from "./lib/broadcasts";
18 | import {
19 | calculateRoundScore,
20 | cardReplaceDiscardPileClick,
21 | cardReplaceDrawPileKeepClick,
22 | cardRevealClick,
23 | checkAllPlayersCardsRevealed,
24 | checkAllPlayersReady,
25 | checkCardsVerticalRow,
26 | checkCardsRevealed,
27 | checkIsLobbyFull,
28 | createGame,
29 | dealCardsToPlayers,
30 | discardCurrentDrawPileCard,
31 | getGame,
32 | getGameByLobby,
33 | getGamesForLobby,
34 | getPlayer,
35 | joinGame,
36 | leaveGame,
37 | setNextActivePlayer,
38 | } from "./lib/games";
39 | import { status } from "./lib/statusMessages";
40 |
41 | export function listenSocket(server): void {
42 | const io = new Server(server, {});
43 | let lobbyNr = 1;
44 |
45 | io.on("connection", (socket: Socket) => {
46 | console.log(socket.id + " connected");
47 |
48 | socket.on("disconnect", async () => {
49 | console.log(socket.id + " disconnected");
50 | });
51 |
52 | socket.on("disconnecting", async () => {
53 | for (let i = 1; i <= 30; i++) {
54 | if (socket.rooms.has(`lobby${i}`)) {
55 | const lobbyNr = (await getGame(socket.id)).lobbyNr;
56 | await leaveGame(socket.id);
57 | broadcastTotalScoresToLobby(io, lobbyNr);
58 | broadcastPlayerCountToLobby(io, lobbyNr);
59 | broadcastPlayersToLobby(io, lobbyNr);
60 | broadcastListGamesUpdate(io);
61 | return;
62 | }
63 | }
64 | });
65 |
66 | socket.on("leave game", async (socketID: string, lobbyNr: number) => {
67 | socket.leave(`lobby${lobbyNr}`);
68 | await leaveGame(socketID);
69 | broadcastListGamesUpdate(io);
70 | broadcastTotalScoresToLobby(io, lobbyNr);
71 | broadcastPlayerCountToLobby(io, lobbyNr);
72 | broadcastPlayersToLobby(io, lobbyNr);
73 | });
74 |
75 | socket.on("get list of games", () => {
76 | socket.emit("display list of games", getGamesForLobby());
77 | });
78 |
79 | socket.on("get scores to display", (lobbyNr: number) => {
80 | broadcastTotalScoresToLobby(io, lobbyNr);
81 | });
82 |
83 | socket.on("get rounds to display", (lobbyNr: number) => {
84 | broadcastRoundNrToLobby(io, lobbyNr);
85 | });
86 |
87 | socket.on("get playercount", (lobbyNr: number) => {
88 | broadcastPlayerCountToLobby(io, lobbyNr);
89 | });
90 |
91 | socket.on("get players", (lobbyNr: number) => {
92 | broadcastPlayersToLobby(io, lobbyNr);
93 | });
94 |
95 | socket.on("get discardpile", (lobbyNr: number) => {
96 | broadcastDiscardPileToLobby(io, lobbyNr);
97 | });
98 |
99 | socket.on("get new drawpilecard", async (lobbyNr: number) => {
100 | broadcastNewDrawPileCardToLobby(io, lobbyNr);
101 | });
102 |
103 | socket.on("get current drawpilecard", async (lobbyNr: number) => {
104 | broadcastCurrentDrawPileCardToLobby(io, lobbyNr);
105 | });
106 |
107 | socket.on("create game", (playerName: string, socketID: string) => {
108 | socket.join(`lobby${lobbyNr}`);
109 | createGame(lobbyNr, playerName, socketID);
110 | socket.emit("pass lobbynr", lobbyNr);
111 | broadcastPlayerCountToLobby(io, lobbyNr);
112 | broadcastTotalScoresToLobby(io, lobbyNr);
113 | broadcastPlayersToLobby(io, lobbyNr);
114 | broadcastListGamesUpdate(io);
115 | lobbyNr++;
116 | });
117 |
118 | socket.on(
119 | "join game",
120 | (lobbyNr: number, playerName: string, socketID: string) => {
121 | socket.join(`lobby${lobbyNr}`);
122 | joinGame(lobbyNr, playerName, socketID);
123 | checkIsLobbyFull(lobbyNr);
124 | broadcastPlayerCountToLobby(io, lobbyNr);
125 | broadcastTotalScoresToLobby(io, lobbyNr);
126 | broadcastPlayersToLobby(io, lobbyNr);
127 | broadcastListGamesUpdate(io);
128 | }
129 | );
130 |
131 | socket.on("player is ready", async (socketID, lobbyNr) => {
132 | const player = await getPlayer(socketID);
133 | player.isReady = true;
134 | broadcastPlayersToLobby(io, lobbyNr);
135 | });
136 |
137 | socket.on("check all players ready", (lobbyNr: number) => {
138 | if (checkAllPlayersReady(lobbyNr)) {
139 | getGameByLobby(lobbyNr).hasStarted = true;
140 | broadcastGameStartToLobby(io, lobbyNr);
141 | dealCardsToPlayers(12, lobbyNr);
142 | broadcastPlayersToLobby(io, lobbyNr);
143 | broadcastDiscardPileToLobby(io, lobbyNr);
144 | broadcastListGamesUpdate(io);
145 | io.to(`lobby${lobbyNr}`).emit("display status", status.PRESTART);
146 | } else {
147 | return;
148 | }
149 | });
150 |
151 | socket.on(
152 | "cardgrid click",
153 | async (socketID: string, lobbyNr: number, index: number) => {
154 | await cardRevealClick(socketID, index);
155 | await calculateRoundScore(socketID, lobbyNr);
156 | broadcastPlayersToLobby(io, lobbyNr);
157 | }
158 | );
159 |
160 | socket.on(
161 | "check 2 cards revealed",
162 | async (socketID: string, lobbyNr: number, callback) => {
163 | const bothCardsRevealed = await checkCardsRevealed(
164 | socketID,
165 | 2,
166 | lobbyNr
167 | );
168 |
169 | if (bothCardsRevealed) {
170 | io.to(socketID).emit("display status", status.PRESTARTWAIT);
171 | }
172 | if (checkAllPlayersCardsRevealed(lobbyNr, 2)) {
173 | await broadcastFirstActivePlayerToLobby(io, lobbyNr, socketID);
174 | await broadcastStatusToActivePlayer(
175 | io,
176 | socketID,
177 | lobbyNr,
178 | status.DRAWDECISION
179 | );
180 | await broadcastTurnPhaseToActivePlayer(
181 | io,
182 | socketID,
183 | lobbyNr,
184 | phase.DRAWDECISION
185 | );
186 | }
187 | callback(bothCardsRevealed);
188 | }
189 | );
190 |
191 | socket.on(
192 | "DRAWDECISION: click discardpile",
193 | async (socketID: string, lobbyNr: number) => {
194 | await broadcastStatusToActivePlayer(
195 | io,
196 | socketID,
197 | lobbyNr,
198 | status.DRAWDISCARDPILEKEEP
199 | );
200 | await broadcastTurnPhaseToActivePlayer(
201 | io,
202 | socketID,
203 | lobbyNr,
204 | phase.DISCARDPILEDECISION
205 | );
206 | }
207 | );
208 |
209 | socket.on(
210 | "DRAWDECISION: click drawpile",
211 | async (socketID: string, lobbyNr: number) => {
212 | await broadcastStatusToActivePlayer(
213 | io,
214 | socketID,
215 | lobbyNr,
216 | status.DRAWPILEDECISION
217 | );
218 | await broadcastTurnPhaseToActivePlayer(
219 | io,
220 | socketID,
221 | lobbyNr,
222 | phase.DRAWPILEDECISION
223 | );
224 | }
225 | );
226 |
227 | socket.on(
228 | "DISCARDPILE: replace card",
229 | async (socketID: string, lobbyNr: number, index: number) => {
230 | await cardReplaceDiscardPileClick(socketID, lobbyNr, index);
231 | await checkCardsVerticalRow(socketID, lobbyNr);
232 | await checkCardsRevealed(socketID, 12, lobbyNr);
233 | await calculateRoundScore(socketID, lobbyNr);
234 | await setNextActivePlayer(socketID, lobbyNr);
235 | broadcastPlayersToLobby(io, lobbyNr);
236 | broadcastDiscardPileToLobby(io, lobbyNr);
237 | broadcastTotalScoresToLobby(io, lobbyNr);
238 | broadcastRoundNrToLobby(io, lobbyNr);
239 | await broadcastTurnStartToActivePlayer(io, socketID, lobbyNr);
240 | //edit
241 | }
242 | );
243 |
244 | socket.on(
245 | "DRAWPILEDECISION: click keep",
246 | async (socketID: string, lobbyNr: number) => {
247 | await broadcastStatusToActivePlayer(
248 | io,
249 | socketID,
250 | lobbyNr,
251 | status.DRAWDISCARDPILEKEEP
252 | );
253 | await broadcastTurnPhaseToActivePlayer(
254 | io,
255 | socketID,
256 | lobbyNr,
257 | phase.DRAWPILEKEEP
258 | );
259 | }
260 | );
261 |
262 | socket.on(
263 | "DRAWPILEDECISION: click discard",
264 | async (socketID: string, lobbyNr: number) => {
265 | await broadcastStatusToActivePlayer(
266 | io,
267 | socketID,
268 | lobbyNr,
269 | status.DRAWPILEDISCARD
270 | );
271 | await broadcastTurnPhaseToActivePlayer(
272 | io,
273 | socketID,
274 | lobbyNr,
275 | phase.DRAWPILEDISCARD
276 | );
277 | discardCurrentDrawPileCard(lobbyNr);
278 | broadcastDiscardPileToLobby(io, lobbyNr);
279 | broadcastCurrentDrawPileCardToLobby(io, lobbyNr);
280 | }
281 | );
282 |
283 | socket.on(
284 | "DRAWPILE: invalid reveal card",
285 | async (socketID: string, lobbyNr: number) => {
286 | await broadcastStatusToActivePlayer(
287 | io,
288 | socketID,
289 | lobbyNr,
290 | status.DRAWPILEDISCARDINVALID
291 | );
292 | }
293 | );
294 |
295 | socket.on(
296 | "DRAWPILE: replace card",
297 | async (socketID: string, lobbyNr: number, index: number) => {
298 | await cardReplaceDrawPileKeepClick(socketID, lobbyNr, index);
299 | await checkCardsVerticalRow(socketID, lobbyNr);
300 | await checkCardsRevealed(socketID, 12, lobbyNr);
301 | await calculateRoundScore(socketID, lobbyNr);
302 | await setNextActivePlayer(socketID, lobbyNr);
303 | broadcastCurrentDrawPileCardToLobby(io, lobbyNr);
304 | broadcastPlayersToLobby(io, lobbyNr);
305 | broadcastDiscardPileToLobby(io, lobbyNr);
306 | broadcastTotalScoresToLobby(io, lobbyNr);
307 | broadcastRoundNrToLobby(io, lobbyNr);
308 | await broadcastTurnStartToActivePlayer(io, socketID, lobbyNr);
309 | //edit
310 | }
311 | );
312 |
313 | socket.on(
314 | "DRAWPILE: reveal card",
315 | async (socketID: string, lobbyNr: number, index: number) => {
316 | await cardRevealClick(socketID, index);
317 | await checkCardsVerticalRow(socketID, lobbyNr);
318 | await checkCardsRevealed(socketID, 12, lobbyNr);
319 | await calculateRoundScore(socketID, lobbyNr);
320 | await setNextActivePlayer(socketID, lobbyNr);
321 | broadcastPlayersToLobby(io, lobbyNr);
322 | broadcastTotalScoresToLobby(io, lobbyNr);
323 | broadcastRoundNrToLobby(io, lobbyNr);
324 | await broadcastTurnStartToActivePlayer(io, socketID, lobbyNr);
325 | //edit
326 | }
327 | );
328 | });
329 | }
330 |
--------------------------------------------------------------------------------
/server/lib/games.ts:
--------------------------------------------------------------------------------
1 | import { generateCards } from "./cards";
2 | import type {
3 | Game,
4 | GamesType,
5 | GameForLobby,
6 | Player,
7 | PlayerScoreList,
8 | PlayerForCardGrid,
9 | Card,
10 | ActivePlayer,
11 | } from "./gameTypes";
12 |
13 | const games: GamesType = {};
14 |
15 | export function createGame(
16 | lobbyNr: number,
17 | playerName: string,
18 | socketID: string
19 | ): void {
20 | games[lobbyNr] = {
21 | lobbyNr,
22 | roundNr: 1,
23 | playerCount: 1,
24 | lobbyIsFull: false,
25 | hasStarted: false,
26 | activePlayerIndex: null,
27 | players: [
28 | {
29 | name: playerName,
30 | socketID: socketID,
31 | isReady: false,
32 | cards: [],
33 | totalScore: 0,
34 | roundScore: [],
35 | allCardsRevealed: false,
36 | },
37 | ],
38 | drawPileCards: generateCards(),
39 | tempDrawPileCard: null,
40 | discardPileCards: [],
41 | currentRoundScores: [],
42 | };
43 | //commented out for now
44 | // console.log(JSON.stringify(games, null, 4));
45 | }
46 |
47 | export function checkIsLobbyFull(lobbyNr: number): void {
48 | if (games[lobbyNr].playerCount >= 8) {
49 | games[lobbyNr].lobbyIsFull = true;
50 | }
51 | }
52 |
53 | export function joinGame(
54 | lobbyNr: number,
55 | playerName: string,
56 | socketID: string
57 | ): void {
58 | console.log(playerName + ":" + socketID + " joined " + lobbyNr);
59 | const playersInGame = games[lobbyNr].players;
60 | playersInGame.push({
61 | name: playerName,
62 | socketID: socketID,
63 | isReady: false,
64 | cards: [],
65 | totalScore: 0,
66 | roundScore: [],
67 | allCardsRevealed: false,
68 | });
69 | games[lobbyNr].playerCount++;
70 | }
71 |
72 | export async function leaveGame(socketID: string): Promise {
73 | const currentGame = await getGame(socketID);
74 |
75 | const currentGamePlayers = currentGame.players.map(
76 | (player) => player.socketID
77 | );
78 | const indexOfTargetPlayer = currentGamePlayers.indexOf(socketID);
79 |
80 | if (indexOfTargetPlayer > -1) {
81 | currentGame.players.splice(indexOfTargetPlayer, 1);
82 | currentGame.playerCount--;
83 | }
84 | }
85 |
86 | export function getGamesForLobby(): GameForLobby[] {
87 | return Object.values(games).map((game) => {
88 | return {
89 | lobbyNr: game.lobbyNr,
90 | playerCount: game.playerCount,
91 | lobbyIsFull: game.lobbyIsFull,
92 | hasStarted: game.hasStarted,
93 | };
94 | });
95 | }
96 |
97 | export async function getGame(socketID: string): Promise {
98 | return Object.values(games).find((game) =>
99 | game.players.find((id) => id.socketID === socketID)
100 | );
101 | }
102 |
103 | export async function getPlayer(socketID: string): Promise {
104 | const currentGame = await getGame(socketID);
105 | const player = currentGame.players.find((id) => id.socketID === socketID);
106 |
107 | return player;
108 | }
109 |
110 | export function getTotalScores(lobbyNr: number): PlayerScoreList[] {
111 | return games[lobbyNr].players.map((player) => {
112 | return { name: player.name, totalScore: player.totalScore };
113 | });
114 | }
115 |
116 | export function getRoundNr(lobbyNr: number): number {
117 | return games[lobbyNr].roundNr;
118 | }
119 |
120 | export function getPlayerCount(lobbyNr: number): number {
121 | return games[lobbyNr].playerCount;
122 | }
123 |
124 | export function getPlayersInLobby(lobbyNr: number): PlayerForCardGrid[] {
125 | return games[lobbyNr].players.map((player) => {
126 | return {
127 | name: player.name,
128 | cards: player.cards,
129 | roundScore: player.roundScore,
130 | socketID: player.socketID,
131 | isReady: player.isReady,
132 | };
133 | });
134 | }
135 |
136 | export function checkAllPlayersReady(lobbyNr: number): boolean {
137 | const players = games[lobbyNr].players;
138 | return players.every((player) => player.isReady);
139 | }
140 |
141 | export function getGameByLobby(lobbyNr: number): Game {
142 | return games[lobbyNr];
143 | }
144 |
145 | export function getDiscardPile(lobbyNr: number): Card {
146 | const discardPile = getGameByLobby(lobbyNr).discardPileCards;
147 | return discardPile[discardPile.length - 1];
148 | }
149 |
150 | // export function getNumberOfGames(): number {
151 | // return games[[0].length];
152 | // }
153 |
154 | export function getRandomCard(lobbyNr: number): Card {
155 | const drawPile = getGameByLobby(lobbyNr).drawPileCards;
156 | const randomIndex = Math.floor(Math.random() * (drawPile.length + 1));
157 | const randomCard = drawPile[randomIndex];
158 | drawPile.splice(randomIndex, 1);
159 | if (randomCard === null) {
160 | getRandomCard(lobbyNr);
161 | } else {
162 | return randomCard;
163 | }
164 | }
165 |
166 | export function dealCardsToPlayers(amount: number, lobbyNr: number): void {
167 | const playerCount = games[lobbyNr].playerCount;
168 |
169 | for (let i = 0; i < playerCount; i++) {
170 | const player = games[lobbyNr].players[i];
171 | for (let j = 1; j <= amount; j++) {
172 | const randomCard = getRandomCard(lobbyNr);
173 | player.cards.push(randomCard);
174 | }
175 | }
176 |
177 | const randomCardDiscardPile = getRandomCard(lobbyNr);
178 | randomCardDiscardPile.hidden = false;
179 | games[lobbyNr].discardPileCards.push(randomCardDiscardPile);
180 | }
181 |
182 | export async function cardRevealClick(
183 | socketID: string,
184 | index: number
185 | ): Promise {
186 | (await getPlayer(socketID)).cards[index].hidden = false;
187 | }
188 |
189 | export async function cardReplaceDiscardPileClick(
190 | socketID: string,
191 | lobbyNr: number,
192 | index: number
193 | ): Promise {
194 | const playerCards = (await getPlayer(socketID)).cards;
195 | const clickedCard = (await getPlayer(socketID)).cards[index];
196 | const indexLastDiscardPileCard = games[lobbyNr].discardPileCards.length - 1;
197 | const discardPileCard =
198 | games[lobbyNr].discardPileCards[indexLastDiscardPileCard];
199 |
200 | const replaceClickedCardWithDiscardPileCard = () => {
201 | clickedCard.hidden = false;
202 | discardPileCard.hidden = false;
203 |
204 | playerCards.splice(index, 0, discardPileCard);
205 | games[lobbyNr].discardPileCards.splice(
206 | indexLastDiscardPileCard,
207 | 1,
208 | clickedCard
209 | );
210 | playerCards.splice(index + 1, 1);
211 | };
212 | replaceClickedCardWithDiscardPileCard();
213 | }
214 |
215 | export async function cardReplaceDrawPileKeepClick(
216 | socketID: string,
217 | lobbyNr: number,
218 | index: number
219 | ): Promise {
220 | const playerCards = (await getPlayer(socketID)).cards;
221 | const clickedCard = (await getPlayer(socketID)).cards[index];
222 | const drawPileCard = games[lobbyNr].tempDrawPileCard;
223 |
224 | const replaceClickedCardWithDrawPileCard = () => {
225 | clickedCard.hidden = false;
226 |
227 | const replacedCard = playerCards.splice(index, 1, drawPileCard);
228 | games[lobbyNr].discardPileCards.push(replacedCard[0]);
229 | games[lobbyNr].tempDrawPileCard = null;
230 | };
231 | replaceClickedCardWithDrawPileCard();
232 | }
233 |
234 | export async function checkCardsVerticalRow(
235 | socketID: string,
236 | lobbyNr: number
237 | ): Promise {
238 | const activePlayer = await getPlayer(socketID);
239 | const cards = activePlayer.cards;
240 | const blankCard = {
241 | value: 0,
242 | imgSrc: "/cards/blank.png",
243 | hidden: false,
244 | };
245 |
246 | function checkRow(cardOne: number, cardTwo: number, cardThree: number): void {
247 | if (
248 | cards[cardOne].hidden === false &&
249 | cards[cardTwo].hidden === false &&
250 | cards[cardThree].hidden === false &&
251 | cards[cardOne].value === cards[cardTwo].value &&
252 | cards[cardOne].value === cards[cardThree].value
253 | ) {
254 | games[lobbyNr].discardPileCards.push(
255 | cards[cardOne],
256 | cards[cardTwo],
257 | cards[cardThree]
258 | );
259 | cards.splice(cardOne, 1, blankCard);
260 | cards.splice(cardTwo, 1, blankCard);
261 | cards.splice(cardThree, 1, blankCard);
262 | }
263 | }
264 |
265 | checkRow(0, 4, 8);
266 | checkRow(1, 5, 9);
267 | checkRow(2, 6, 10);
268 | checkRow(3, 7, 11);
269 | }
270 |
271 | export async function calculateRoundScore(socketID: string, lobbyNr: number) {
272 | const player = await getPlayer(socketID);
273 | const roundNr = games[lobbyNr].roundNr;
274 | let roundScore = null;
275 |
276 | player.cards.forEach((card) => {
277 | if (card.hidden === false) {
278 | roundScore = roundScore + card.value;
279 | }
280 | });
281 |
282 | player.roundScore[roundNr - 1] = roundScore;
283 | }
284 |
285 | export async function calculateTotalScores(socketID: string): Promise {
286 | const currentGame = await getGame(socketID);
287 |
288 | function checkFinishingScore(currentGame: Game) {
289 | const firstPlayerFinished = currentGame.players.find(
290 | (player) => player.socketID === currentGame.currentRoundScores[0].socketID
291 | );
292 |
293 | const remainingPlayers = currentGame.players.filter(
294 | (player) => player.socketID !== firstPlayerFinished.socketID
295 | );
296 |
297 | const firstPlayerScore =
298 | firstPlayerFinished.roundScore[firstPlayerFinished.roundScore.length - 1];
299 |
300 | if (
301 | remainingPlayers.some(
302 | (player) =>
303 | firstPlayerScore >= player.roundScore[player.roundScore.length - 1]
304 | )
305 | ) {
306 | firstPlayerFinished.roundScore[
307 | firstPlayerFinished.roundScore.length - 1
308 | ] = firstPlayerScore * 2;
309 | }
310 | }
311 |
312 | function getPlayerRoundScores(index: number): number[] {
313 | return currentGame.players[index].roundScore;
314 | }
315 |
316 | function calculateTotalScore(roundScores: number[]): number {
317 | return roundScores.reduce((a, b) => {
318 | return a + b;
319 | });
320 | }
321 |
322 | checkFinishingScore(currentGame);
323 | currentGame.players.forEach((player, index) => {
324 | const playerRoundScores = getPlayerRoundScores(index);
325 | const newTotalScore = calculateTotalScore(playerRoundScores);
326 |
327 | player.totalScore = newTotalScore;
328 | });
329 | }
330 |
331 | export async function checkCardsRevealed(
332 | socketID: string,
333 | amount: number,
334 | lobbyNr: number
335 | ): Promise {
336 | const player = await getPlayer(socketID);
337 | const revealedCards = player.cards.filter((card) => card.hidden === false);
338 |
339 | if (revealedCards.length === amount) {
340 | if (revealedCards.length === 12) {
341 | player.allCardsRevealed = true;
342 | games[lobbyNr].currentRoundScores.push({
343 | socketID: player.socketID,
344 | roundScore: player.roundScore,
345 | });
346 | }
347 | return true;
348 | } else {
349 | return false;
350 | }
351 | }
352 |
353 | export function checkAllPlayersCardsRevealed(
354 | lobbyNr: number,
355 | amount: number
356 | ): boolean {
357 | const currentGame = getGameByLobby(lobbyNr);
358 | return currentGame.players.every((player) => {
359 | const revealedCards = player.cards.filter((card) => card.hidden === false);
360 | if (revealedCards.length === amount) {
361 | return true;
362 | }
363 | });
364 | }
365 |
366 | export function getCurrentRoundscores(lobbyNr: number): number[] {
367 | const currentGame = getGameByLobby(lobbyNr);
368 | return currentGame.players.map((player) => {
369 | const lastScore = player.roundScore.length - 1;
370 | return player.roundScore[lastScore];
371 | });
372 | }
373 |
374 | export async function getFirstActivePlayer(
375 | lobbyNr: number,
376 | socketid: string
377 | ): Promise {
378 | const currentGame = await getGame(socketid);
379 | const roundScores = getCurrentRoundscores(lobbyNr);
380 | const indexHighestRoundScore = roundScores.indexOf(Math.max(...roundScores));
381 | currentGame.activePlayerIndex = indexHighestRoundScore;
382 | const { name, socketID, roundScore } = currentGame.players[
383 | indexHighestRoundScore
384 | ];
385 |
386 | const activePlayer: ActivePlayer = {
387 | name: name,
388 | socketID: socketID,
389 | roundScore: roundScore,
390 | };
391 | return activePlayer;
392 | }
393 |
394 | export async function getActivePlayer(socketID: string): Promise {
395 | const currentGame = await getGame(socketID);
396 | const indexActivePlayer = currentGame.activePlayerIndex;
397 | const activePlayer = currentGame.players[indexActivePlayer];
398 | return activePlayer;
399 | }
400 |
401 | export async function setNextActivePlayer(
402 | socketID: string,
403 | lobbyNr: number
404 | ): Promise {
405 | const currentGame = await getGame(socketID);
406 | const indexCurrentActivePlayer = currentGame.activePlayerIndex;
407 |
408 | if (currentGame.players[indexCurrentActivePlayer + 1]) {
409 | const nextActivePlayer = currentGame.players[indexCurrentActivePlayer + 1];
410 | const indexNextActivePlayer = currentGame.players.indexOf(nextActivePlayer);
411 | currentGame.activePlayerIndex = indexNextActivePlayer;
412 | await handleRoundEnd(socketID, lobbyNr);
413 | } else {
414 | currentGame.activePlayerIndex = 0;
415 | await handleRoundEnd(socketID, lobbyNr);
416 | }
417 | }
418 |
419 | export async function handleRoundEnd(
420 | socketID: string,
421 | lobbyNr: number
422 | ): Promise {
423 | const currentGame = await getGame(socketID);
424 | const activePlayer = currentGame.players[currentGame.activePlayerIndex];
425 |
426 | if (activePlayer.allCardsRevealed === true) {
427 | await revealAllCards(socketID);
428 | await calculateRoundScore(socketID, lobbyNr);
429 | await calculateTotalScores(socketID);
430 | currentGame.roundNr = currentGame.roundNr + 1;
431 |
432 | // Maybe reset game cards and deal new cards on newRoundStart.
433 | // Reset and deal new cards when the roundScore Modal is continued by all players in frontend
434 | // reset game cards?
435 | // deal new cards?
436 | console.log("current Game", JSON.stringify(currentGame, null, 4));
437 | }
438 | }
439 |
440 | export function getNewDrawPileCard(lobbyNr: number): Card {
441 | const randomCard = getRandomCard(lobbyNr);
442 | randomCard.hidden = false;
443 | games[lobbyNr].tempDrawPileCard = randomCard;
444 | return randomCard;
445 | }
446 |
447 | export function getCurrentDrawPileCard(lobbyNr: number): Card {
448 | return games[lobbyNr].tempDrawPileCard;
449 | }
450 |
451 | export function discardCurrentDrawPileCard(lobbyNr: number): void {
452 | const drawPileCard = games[lobbyNr].tempDrawPileCard;
453 | games[lobbyNr].discardPileCards.push(drawPileCard);
454 | games[lobbyNr].tempDrawPileCard = null;
455 | }
456 |
457 | export async function revealAllCards(socketID: string): Promise {
458 | const currentGame = await getGame(socketID);
459 |
460 | function revealCards(index: number): void {
461 | currentGame.players[index].cards.forEach((card) => {
462 | if (card.hidden) {
463 | card.hidden = false;
464 | }
465 | });
466 | }
467 |
468 | for (let i = 0; i < currentGame.playerCount; i++) {
469 | revealCards(i);
470 | }
471 | }
472 |
473 | // Funktion für letzte TURNS.
474 | // Wenn alle 12 cards revealed true is, soll die Funktion ausgeführt werden.
475 | // Bei setNextActivePlayer() checken ob der nächste Player allCardsRevealed === true hat.
476 | // Wenn ja, dann soll das Spielende eingeleitet werden: Alle übrigen Karten sollen aufgedeckt werden und die
477 | // Round Scores ermittelt und gespeichert werden. Popup soll die Runde zusammenfassen.
478 | // An Frontend gameHasStarted / roundstart auf false setzen, damit dort nich geklickt werden kann.
479 | // Bzw. kann es nach wegklicken des Popups ja direkt weitergehen mit Karten aufdecken etc.
480 | // Der Spieler der allCardsRevealed===true hatte, sollte in der nächsten Runde anfangen nach aufdecken der Karten.
481 |
--------------------------------------------------------------------------------