From 7428faa138925d2a76639b93aecb15eae49a4402 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 17 Sep 2024 23:08:02 +0200 Subject: [PATCH] Anpassungen bzgl. Stich-Anzeige und Kartenreihenfolge auf dem Stapel --- .eslintrc.json | 45 +++++ .prettierrc.json | 18 ++ .vscode/launch.json | 13 +- .vscode/settings.json | 29 ++++ src/app/card-back.component.ts | 1 - src/app/card.service.ts | 1 + src/app/deck.component.ts | 49 ------ src/app/game-rule-engine.ts | 68 ++++++++ src/app/game.component.html | 54 ++++++ src/app/game.component.scss | 101 +++++++++++ src/app/game.component.ts | 309 +++++++++++++++++---------------- 11 files changed, 488 insertions(+), 200 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .prettierrc.json create mode 100644 .vscode/settings.json delete mode 100644 src/app/deck.component.ts create mode 100644 src/app/game-rule-engine.ts create mode 100644 src/app/game.component.html create mode 100644 src/app/game.component.scss diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..eb7cda9 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,45 @@ +{ + "env": { + "es2021": true, + "browser": true + }, + "extends": [ + "airbnb-base", + "airbnb-typescript", + "plugin:@typescript-eslint/recommended", + "eslint-config-prettier", + "plugin:cypress/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module", + "project": ["./tsconfig.json"] + }, + "plugins": ["@typescript-eslint"], + "rules": { + "import/no-unresolved": ["off"], + "import/prefer-default-export": ["off"], + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": ["error"], + "@typescript-eslint/lines-between-class-members": ["off"], + "no-param-reassign": ["off"], + "max-classes-per-file": ["off"], + "no-shadow": ["off"], + "class-methods-use-this": ["off"], + "react/jsx-filename-extension": ["off"], + "import/no-cycle": ["off"], + "radix": ["off"], + "no-promise-executor-return": ["off"], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "enumMember", + "format": ["UPPER_CASE", "PascalCase"] + } + ], + "no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"], + "spaced-comment": ["off"], + "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] + } + } \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..285ba9d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,18 @@ +{ + "arrowParens": "avoid", + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "printWidth": 220, + "proseWrap": "always", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false, + "vueIndentScriptAndStyle": false +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 925af83..b670a46 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,11 +10,18 @@ "url": "http://localhost:4200/" }, { - "name": "ng test", "type": "chrome", "request": "launch", - "preLaunchTask": "npm: test", - "url": "http://localhost:9876/debug.html" + "name": "Debug Tests", + "url": "http://localhost:9876/debug.html", + "webRoot": "${workspaceFolder}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:/*": "${webRoot}/*" + }, + "runtimeArgs": [ + "--headless" + ] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c3f52a3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,29 @@ +{ + "editor.suggestSelection": "first", + "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", + "explorer.confirmDelete": false, + "typescript.updateImportsOnFileMove.enabled": "always", + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[scss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "prettier.printWidth": 240, + "git.autofetch": false, + "git.autorefresh": true + } + \ No newline at end of file diff --git a/src/app/card-back.component.ts b/src/app/card-back.component.ts index 6225287..3a25c3a 100644 --- a/src/app/card-back.component.ts +++ b/src/app/card-back.component.ts @@ -67,7 +67,6 @@ import { CommonModule } from '@angular/common'; color: green; font-weight: bold; font-size: 24px; - z-index: 2; } .logo.top { top: 20%; diff --git a/src/app/card.service.ts b/src/app/card.service.ts index 2e388f4..b43c04c 100644 --- a/src/app/card.service.ts +++ b/src/app/card.service.ts @@ -2,6 +2,7 @@ export interface Card { number: number; color: string; + playable?: boolean; } // card.service.ts diff --git a/src/app/deck.component.ts b/src/app/deck.component.ts deleted file mode 100644 index a5622ee..0000000 --- a/src/app/deck.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { CardBackComponent } from './card-back.component'; -import { CardFrontComponent } from './card-front.component'; -import { Card, CardService } from './card.service'; - - -@Component({ - selector: 'app-deck', - standalone: true, - imports: [CommonModule, CardFrontComponent, CardBackComponent], - template: ` -
-
- - -
-
- - `, - styles: [` - .deck { - display: flex; - flex-wrap: wrap; - gap: 10px; - } - `] -}) -export class DeckComponent implements OnInit { - deck: Card[] = []; - flippedCards: boolean[] = []; - - constructor(private cardService: CardService) {} - - ngOnInit() { - // this.deck = this.cardService.getDeck(); - // this.flippedCards = new Array(this.deck.length).fill(false); - } - - // flipCard(index: number) { - // this.flippedCards[index] = !this.flippedCards[index]; - // } - - // shuffleDeck() { - // this.cardService.shuffleDeck(); - // this.deck = this.cardService.getDeck(); - // this.flippedCards = new Array(this.deck.length).fill(false); - // } -} \ No newline at end of file diff --git a/src/app/game-rule-engine.ts b/src/app/game-rule-engine.ts new file mode 100644 index 0000000..5ac5a55 --- /dev/null +++ b/src/app/game-rule-engine.ts @@ -0,0 +1,68 @@ +import { Card, CardService } from './card.service'; +import { Player } from './game.component'; + + +export class GameRuleEngine { + private trumpColor: string = 'red'; + + constructor(private cardService: CardService) {} + + determineStartingPlayer(roundNumber: number, playerCount: number): number { + return (roundNumber - 1) % playerCount; + } + + determineWinner(playedCards: Card[], players: Player[]): Player { + let winningCard:Card = playedCards[0]; + const leadColor = playedCards[0].color; + + for (let i = 1; i < playedCards.length; i++) { + if (this.isWinningCard(playedCards[i], winningCard, leadColor)) { + winningCard=playedCards[i] + } + } + + return players.find(p=>p.playedCard?.color===winningCard.color && p.playedCard.number===winningCard.number) ||players[0] + } + + private isWinningCard(card: Card, currentWinningCard: Card, leadColor: string): boolean { + // Wenn beide Karten Trumpf sind, gewinnt die höhere + if (card.color === this.trumpColor && currentWinningCard.color === this.trumpColor) { + return card.number > currentWinningCard.number; + } + + // Wenn nur die neue Karte Trumpf ist, gewinnt sie + if (card.color === this.trumpColor && currentWinningCard.color !== this.trumpColor) { + return true; + } + + // Wenn die aktuelle Gewinnerkarte Trumpf ist, kann keine andere Farbe gewinnen + if (currentWinningCard.color === this.trumpColor) { + return false; + } + + // Wenn keine der Karten Trumpf ist, gewinnt die höhere Karte der Ausgangsfarbe + if (card.color === leadColor && currentWinningCard.color === leadColor) { + return card.number > currentWinningCard.number; + } + + // Wenn nur die neue Karte die Ausgangsfarbe hat, gewinnt sie + if (card.color === leadColor && currentWinningCard.color !== leadColor) { + return true; + } + + // In allen anderen Fällen gewinnt die neue Karte nicht + return false; + } + + canPlayCard(card: Card, hand: Card[], leadCard: Card | null): boolean { + if (!leadCard) return true; // Erste Karte des Stichs + if (card.color === leadCard.color) return true; // Gleiche Farbe wie die Ausgangskarte + return !hand.some(c => c.color === leadCard.color); // Keine Karte der Ausgangsfarbe in der Hand + } + + getPlayableCards(hand: Card[], leadCard: Card | null): Card[] { + if (!leadCard) return hand; + const sameColorCards = hand.filter(card => card.color === leadCard.color); + return sameColorCards.length > 0 ? sameColorCards : hand; + } + } \ No newline at end of file diff --git a/src/app/game.component.html b/src/app/game.component.html new file mode 100644 index 0000000..296a661 --- /dev/null +++ b/src/app/game.component.html @@ -0,0 +1,54 @@ +
+
+ + + +
+ + @if (gameStarted) { +
+ @for (opponent of opponents; track opponent; let i = $index) { +
+ @for (card of opponent.hand; track card; let cardIndex = $index) { + + } +
+ } +
+
+
+ @for (playedCard of playedCards; track playedCard; let i = $index) { @if(i===0){ + + } @if(i>0){ + + } } +
+
+
+ @for (card of player.hand; track card; let i = $index) { + + } +
+
+
Spieler: {{ player.score }} @for (opponent of opponents; track opponent; let i = $index) { | Gegner {{ i + 1 }}: {{ opponent.score }} } | Runde: {{ currentRound }}/10
+ + + + + + + + + + + + + + + + + +
RundeSpielerGegner {{ opponent.id }}
{{ stat.round }}{{ stat.playerStitches }}{{ stitches }}
+
+ } +
diff --git a/src/app/game.component.scss b/src/app/game.component.scss new file mode 100644 index 0000000..4cefe92 --- /dev/null +++ b/src/app/game.component.scss @@ -0,0 +1,101 @@ +.game { + display: flex; + flex-direction: column; + height: 100vh; + padding: 20px; + box-sizing: border-box; + overflow: hidden; +} +.game-setup { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} +.opponents-area { + display: flex; + justify-content: space-around; + min-height: 320px; +} +.opponent-hand { + position: relative; + width: 200px; + height: 280px; +} +.stacked-card { + position: absolute; + top: 0; +} +.player-hand { + display: flex; + gap: 10px; + min-height: 320px; + align-items: center; + justify-content: center; +} +.play-area { + flex: 1 1 auto; + min-height: 200px; + display: flex; + justify-content: center; + align-items: center; +} +.played-cards { + position: relative; + width: 140px; + height: 200px; +} +.player-played-card { + position: absolute; + bottom: 0; + left: 0; +} +.opponent-played-card { + position: absolute; +} +.disabled { + opacity: 0.5; + pointer-events: none; +} +.game-info { + height: 10%; + min-height: 80px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; +} +.score { + font-size: 1.2em; + font-weight: bold; +} +.round-stats { + position: fixed; + right: 20px; + top: 20px; + width: 250px; + background-color: #f9f9f9; + border: 1px solid #ccc; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + font-size: 14px; + z-index: 100; + text-align: center; +} + +.round-stats th, +.round-stats td { + padding: 8px; + border-bottom: 1px solid #ddd; +} + +.round-stats thead { + background-color: #f1f1f1; + font-weight: bold; +} + +.round-stats tbody tr:last-child td { + border-bottom: none; +} diff --git a/src/app/game.component.ts b/src/app/game.component.ts index 1fb9266..29d7200 100644 --- a/src/app/game.component.ts +++ b/src/app/game.component.ts @@ -1,112 +1,64 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { CardFrontComponent } from './card-front.component'; import { CardBackComponent } from './card-back.component'; import { CardService, Card } from './card.service'; +import { GameRuleEngine } from './game-rule-engine'; +export interface Player { + id: number; + hand: Card[]; + playedCard: Card | null; + score: number; +} +interface RoundStats { + round: number; + playerStitches: number; + opponentStitches: number[]; +} @Component({ selector: 'app-game', standalone: true, - imports: [CommonModule, CardFrontComponent, CardBackComponent], - template: ` -
-
- -
-
-
- - -
-
-
- - -
-
-
- Spieler: {{playerScore}} | Gegner: {{opponentScore}} | Runde: {{currentRound}}/10 -
- -
-
- `, - styles: [` - .game { - display: flex; - flex-direction: column; - height: 100vh; - padding: 20px; - box-sizing: border-box; - overflow: hidden; - } - .opponent-hand, .player-hand { - display: flex; - gap: 10px; - min-height: 320px; - align-items: center; - justify-content: center; - } - .play-area { - flex: 1 1 auto; - min-height: 200px; - display: flex; - justify-content: center; - align-items: center; - } - .played-cards { - position: relative; - width: 140px; - height: 200px; - } - .player-played-card { - position: absolute; - bottom: 0; - left: 0; - } - .opponent-played-card { - position: absolute; - top: 0; - right: 0; - } - .disabled { - opacity: 0.5; - pointer-events: none; - } - .game-info { - height: 10%; - min-height: 80px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; - } - .score { - font-size: 1.2em; - font-weight: bold; - } - `] + imports: [CommonModule, FormsModule, CardFrontComponent, CardBackComponent], + templateUrl: './game.component.html', + styleUrls: ['./game.component.scss'] }) export class GameComponent implements OnInit { - playerHand: Card[] = []; - opponentHand: Card[] = []; - playerPlayedCard: Card | null = null; - opponentPlayedCard: Card | null = null; - playerScore: number = 0; - opponentScore: number = 0; + player: Player = { id: 0, hand: [], playedCard: null, score: 0 }; + opponents: Player[] = []; currentRound: number = 0; + numberOfOpponents: number = 1; + gameStarted: boolean = false; + currentPlayerIndex: number = 0; + playedCards: Card[] = []; + leadCard: Card | null = null; + roundStats: RoundStats[] = []; + private ruleEngine: GameRuleEngine; - constructor(private cardService: CardService) {} + constructor(private cardService: CardService) { + this.ruleEngine = new GameRuleEngine(cardService); + } - ngOnInit() { + ngOnInit() {} + + startGame() { + if (this.numberOfOpponents < 1 || this.numberOfOpponents > 3) { + alert("Bitte wählen Sie 1 bis 3 Gegner."); + return; + } + this.gameStarted = true; + this.initializePlayers(); this.startNewRound(); } + initializePlayers() { + this.opponents = []; + for (let i = 0; i < this.numberOfOpponents; i++) { + this.opponents.push({ id: i + 1, hand: [], playedCard: null, score: 0 }); + } + } + startNewRound() { if (this.currentRound >= 10) { this.endGame(); @@ -117,10 +69,107 @@ export class GameComponent implements OnInit { const cardsForThisRound = this.getCardsForRound(this.currentRound); this.cardService.shuffleDeck(); - this.playerHand = this.cardService.drawCards(cardsForThisRound); - this.opponentHand = this.cardService.drawCards(cardsForThisRound); - this.playerPlayedCard = null; - this.opponentPlayedCard = null; + this.player.hand = this.cardService.drawCards(cardsForThisRound); + this.opponents.forEach(opponent => { + opponent.hand = this.cardService.drawCards(cardsForThisRound); + }); + + this.resetPlayedCards(); + this.currentPlayerIndex = this.ruleEngine.determineStartingPlayer(this.currentRound, this.opponents.length + 1); + this.playTrick(); + } + + playTrick() { + if (this.currentPlayerIndex === 0) { + // Player's turn + this.enablePlayableCards(); + } else { + // Opponent's turn + setTimeout(() => this.playOpponentCard(), 1000); + } + } + + enablePlayableCards() { + const playableCards = this.ruleEngine.getPlayableCards(this.player.hand, this.leadCard); + this.player.hand.forEach(card => { + card.playable = playableCards.includes(card); + }); + } + + playCard(index: number) { + const card = this.player.hand[index]; + if (!card.playable) return; + + this.player.playedCard = this.player.hand.splice(index, 1)[0]; + this.playedCards.push(this.player.playedCard); + //this.playedCards.unshift(this.player.playedCard); // Füge die Karte an den Anfang anstatt ans Ende hinzu + if (!this.leadCard) this.leadCard = this.player.playedCard; + + this.moveToNextPlayer(); + } + + playOpponentCard() { + const opponent = this.opponents[this.currentPlayerIndex - 1]; + const playableCards = this.ruleEngine.getPlayableCards(opponent.hand, this.leadCard); + const randomIndex = Math.floor(Math.random() * playableCards.length); + opponent.playedCard = playableCards[randomIndex]; + opponent.hand = opponent.hand.filter(card => card !== opponent.playedCard); + this.playedCards.push(opponent.playedCard); + //this.playedCards.unshift(opponent.playedCard); // Füge die Karte an den Anfang anstatt ans Ende hinzu + if (!this.leadCard) this.leadCard = opponent.playedCard; + + this.moveToNextPlayer(); + } + + moveToNextPlayer() { + if (this.playedCards.length === this.opponents.length + 1) { + // All players have played their cards + setTimeout(() => this.evaluateTrick(), 1000); + } else { + this.currentPlayerIndex = (this.currentPlayerIndex + 1) % (this.opponents.length + 1); + this.playTrick(); + } + } + + evaluateTrick() { + const allPlayers = [this.player, ...this.opponents]; + const winner = this.ruleEngine.determineWinner(this.playedCards, allPlayers); + winner.score++; + + // Neue Logik, um Stiche zu speichern + if (!this.roundStats[this.currentRound - 1]) { + this.roundStats[this.currentRound - 1] = { + round: this.currentRound, + playerStitches: 0, + opponentStitches: new Array(this.opponents.length).fill(0), + }; + } + + if (winner === this.player) { + this.roundStats[this.currentRound - 1].playerStitches++; + } else { + const opponentIndex = this.opponents.indexOf(winner); + this.roundStats[this.currentRound - 1].opponentStitches[opponentIndex]++; + } + + if (this.player.hand.length === 0) { + if (this.currentRound < 10) { + setTimeout(() => this.startNewRound(), 500); + } else { + this.endGame(); + } + } else { + this.resetPlayedCards(); + this.currentPlayerIndex = allPlayers.indexOf(winner); + this.playTrick(); + } + } + + resetPlayedCards() { + this.playedCards = []; + this.leadCard = null; + this.player.playedCard = null; + this.opponents.forEach(opponent => opponent.playedCard = null); } getCardsForRound(round: number): number { @@ -128,57 +177,23 @@ export class GameComponent implements OnInit { return 11 - round; } - playCard(index: number) { - if (this.playerPlayedCard) return; - - this.playerPlayedCard = this.playerHand.splice(index, 1)[0]; - setTimeout(() => this.opponentPlay(), 1000); - } - - opponentPlay() { - if (this.opponentHand.length === 0) return; - - const randomIndex = Math.floor(Math.random() * this.opponentHand.length); - this.opponentPlayedCard = this.opponentHand.splice(randomIndex, 1)[0]; - - setTimeout(() => this.evaluatePlay(), 1000); - } - - evaluatePlay() { - if (!this.playerPlayedCard || !this.opponentPlayedCard) return; - - const result = this.cardService.compareCards(this.playerPlayedCard, this.opponentPlayedCard); - if (result > 0) { - this.playerScore++; - } else if (result < 0) { - this.opponentScore++; - } - - if (this.playerHand.length === 0) { - if (this.currentRound < 10) { - setTimeout(() => { - alert(`Runde ${this.currentRound} beendet! Spieler: ${this.playerScore}, Gegner: ${this.opponentScore}`); - this.playerPlayedCard = null; - this.opponentPlayedCard = null; - }, 500); - } else { - this.endGame(); - } - } else { - setTimeout(() => { - this.playerPlayedCard = null; - this.opponentPlayedCard = null; - }, 1500); - } - } - endGame() { - let result = "Unentschieden!"; - if (this.playerScore > this.opponentScore) { - result = "Sie haben gewonnen!"; - } else if (this.playerScore < this.opponentScore) { - result = "Der Gegner hat gewonnen!"; - } - alert(`Spiel beendet! ${result}\nEndstand - Spieler: ${this.playerScore}, Gegner: ${this.opponentScore}`); + const allPlayers = [this.player, ...this.opponents]; + const maxScore = Math.max(...allPlayers.map(p => p.score)); + const winners = allPlayers.filter(p => p.score === maxScore); + + let result = winners.length > 1 ? "Unentschieden!" : + winners[0] === this.player ? "Sie haben gewonnen!" : + `Gegner ${winners[0].id} hat gewonnen!`; + + let scoreMessage = `Endstand - Spieler: ${this.player.score}`; + this.opponents.forEach((opponent) => { + scoreMessage += `, Gegner ${opponent.id}: ${opponent.score}`; + }); + + alert(`Spiel beendet! ${result}\n${scoreMessage}`); + this.gameStarted = false; + this.currentRound = 0; + this.player.score = 0; } } \ No newline at end of file