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
+
+
+
+
+
+ | Runde |
+ Spieler |
+ Gegner {{ 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