Anpassungen bzgl. Stich-Anzeige und Kartenreihenfolge auf dem Stapel

This commit is contained in:
Andreas Knuth 2024-09-17 23:08:02 +02:00
parent 929eeb15e6
commit 7428faa138
11 changed files with 488 additions and 200 deletions

45
.eslintrc.json Normal file
View File

@ -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 }]
}
}

18
.prettierrc.json Normal file
View File

@ -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
}

13
.vscode/launch.json vendored
View File

@ -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"
]
}
]
}

29
.vscode/settings.json vendored Normal file
View File

@ -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
}

View File

@ -67,7 +67,6 @@ import { CommonModule } from '@angular/common';
color: green;
font-weight: bold;
font-size: 24px;
z-index: 2;
}
.logo.top {
top: 20%;

View File

@ -2,6 +2,7 @@
export interface Card {
number: number;
color: string;
playable?: boolean;
}
// card.service.ts

View File

@ -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: `
<div class="deck">
<div *ngFor="let card of deck; let i = index" (click)="flipCard(i)">
<app-card-front *ngIf="flippedCards[i]" [number]="card.number" [color]="card.color"></app-card-front>
<app-card-back *ngIf="!flippedCards[i]"></app-card-back>
</div>
</div>
<button (click)="shuffleDeck()">Mischen</button>
`,
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);
// }
}

View File

@ -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;
}
}

View File

@ -0,0 +1,54 @@
<div class="game">
<div class="game-setup" *ngIf="!gameStarted">
<label for="opponentCount">Anzahl der Gegner (1-3):</label>
<input type="number" id="opponentCount" [(ngModel)]="numberOfOpponents" min="1" max="3" />
<button (click)="startGame()">Spiel starten</button>
</div>
@if (gameStarted) {
<div class="opponents-area">
@for (opponent of opponents; track opponent; let i = $index) {
<div class="opponent-hand">
@for (card of opponent.hand; track card; let cardIndex = $index) {
<app-card-back [style.left.px]="cardIndex * 30" class="stacked-card"> </app-card-back>
}
</div>
}
</div>
<div class="play-area">
<div class="played-cards">
@for (playedCard of playedCards; track playedCard; let i = $index) { @if(i===0){
<app-card-front *ngIf="playedCard" [number]="playedCard.number" [color]="playedCard.color" class="player-played-card"> </app-card-front>
} @if(i>0){
<app-card-front [number]="playedCard.number" [color]="playedCard.color" [style.top.px]="i * 30" [style.left.px]="i * 30" class="opponent-played-card"> </app-card-front>
} }
</div>
</div>
<div class="player-hand">
@for (card of player.hand; track card; let i = $index) {
<app-card-front [number]="card.number" [color]="card.color" (click)="playCard(i)" [class.disabled]="player.playedCard"> </app-card-front>
}
</div>
<div class="game-info">
<div class="score">Spieler: {{ player.score }} @for (opponent of opponents; track opponent; let i = $index) { | Gegner {{ i + 1 }}: {{ opponent.score }} } | Runde: {{ currentRound }}/10</div>
<button (click)="startNewRound()" [disabled]="player.hand.length > 0 || currentRound > 10">Nächste Runde</button>
<!-- Tabelle für die Stiche pro Runde -->
<table class="round-stats">
<thead>
<tr>
<th>Runde</th>
<th>Spieler</th>
<th *ngFor="let opponent of opponents">Gegner {{ opponent.id }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let stat of roundStats">
<td>{{ stat.round }}</td>
<td>{{ stat.playerStitches }}</td>
<td *ngFor="let stitches of stat.opponentStitches">{{ stitches }}</td>
</tr>
</tbody>
</table>
</div>
}
</div>

101
src/app/game.component.scss Normal file
View File

@ -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;
}

View File

@ -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: `
<div class="game">
<div class="opponent-hand">
<app-card-back *ngFor="let card of opponentHand"></app-card-back>
</div>
<div class="play-area">
<div class="played-cards" *ngIf="playerPlayedCard || opponentPlayedCard">
<app-card-front *ngIf="playerPlayedCard" [number]="playerPlayedCard.number" [color]="playerPlayedCard.color" class="player-played-card"></app-card-front>
<app-card-front *ngIf="opponentPlayedCard" [number]="opponentPlayedCard.number" [color]="opponentPlayedCard.color" class="opponent-played-card"></app-card-front>
</div>
</div>
<div class="player-hand">
<app-card-front *ngFor="let card of playerHand; let i = index"
[number]="card.number"
[color]="card.color"
(click)="playCard(i)"
[class.disabled]="playerPlayedCard">
</app-card-front>
</div>
<div class="game-info">
<div class="score">
Spieler: {{playerScore}} | Gegner: {{opponentScore}} | Runde: {{currentRound}}/10
</div>
<button (click)="startNewRound()" [disabled]="playerHand.length > 0 || currentRound > 10">Nächste Runde</button>
</div>
</div>
`,
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;
}
}