Anonymous
Как заставить игрока и боты выбрать карты, чтобы передать им игрока по часовой стрелке?
Сообщение
Anonymous » 03 май 2025, 18:29
Я пытаюсь выполнить задание, в котором я должен создать карт Game Game of Hearts. Кнопка «пройти». Тем не менее, я не могу заставить кнопку прохода появиться. Я также считаю, что должен поместить каждую карту в контейнер, чтобы сделать их кликабельными, но я не на 100%, как это реализовать. Я должен только обновлять представление, а не модель или контроллер.
Вот модель:
Код: Выделить всё
import {HU} from "./hearts_utils.js";
export class HeartsModel extends EventTarget {
#playerNames;
#state;
#scorelog;
#hands;
#current_trick;
#collected_tricks;
#passing;
constructor() {
super();
this.#playerNames = {
north: null,
east: null,
south: null,
west: null
};
this.#state = 'uninitialized';
this.#scorelog = [];
this.#hands = {
north: null,
east: null,
south: null,
west: null
};
this.#collected_tricks = {
north: [],
east: [],
south: [],
west: []
};
}
// Private methods
#dealCards () {
let deck = [];
['spades', 'hearts', 'diamonds', 'clubs'].forEach(suit => {
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].forEach(rank => {
deck.push(new Card(suit, rank));
})
});
deck = deck.map((card) => [card, Math.random()]).sort((a,b) => a[1]-b[1]).map((cpair) => cpair[0]);
return {
north: new Hand(deck.slice(0,13)),
east: new Hand(deck.slice(13,26)),
south: new Hand(deck.slice(26,39)),
west: new Hand(deck.slice(39,52))
};
}
// These methods are only available to controller to update model.
// They should never be called from view objects.
initialize (north_name, east_name, south_name, west_name) {
if (this.#state != 'uninitialized') return;
this.#playerNames.north = north_name;
this.#playerNames.east = east_name;
this.#playerNames.south = south_name;
this.#playerNames.west = west_name;
this.#scorelog = [];
this.setupGame('right');
};
passCards (cards_to_pass) {
if (this.#state != 'passing') return;
HU.positions.forEach(p => this.#hands[p].remove(cards_to_pass[p]));
HU.positions.forEach(p => {
let pass_to = HU.passing_maps[this.#passing][p];
this.#hands[pass_to].add(cards_to_pass[p]);
});
this.#state = 'playing';
let lead = HU.positions.find(p => this.#hands[p].contains(Card.TWO_OF_CLUBS));
this.#current_trick = new Trick(lead);
this.dispatchEvent(new Event('stateupdate'));
this.dispatchEvent(new Event('trickstart'));
};
playCardIntoTrick (position, card) {
if (this.#state != 'playing') return;
this.#hands[position].remove([card]);
this.#current_trick.playCard(card);
this.dispatchEvent(new CustomEvent('trickplay', {detail: {
position: position,
card: card
}}));
if (this.#current_trick.isComplete()) {
this.dispatchEvent(new Event('trickend'));
}
};
collectTrick (position) {
if (this.#state != 'playing') return;
this.#collected_tricks[position].push(this.#current_trick);
this.dispatchEvent(new CustomEvent('trickcollected', {detail: {
position: position,
trick: this.#current_trick}
}));
this.#current_trick = null;
};
setupTrick (position) {
if (this.#state != 'playing') return;
this.#current_trick = new Trick(position);
this.dispatchEvent(new Event('trickstart'));
};
updateScoreLog (scorelog_entry, moonshooter) {
this.#scorelog.push(scorelog_entry);
this.dispatchEvent(new CustomEvent('scoreupdate', {detail: {
entry: scorelog_entry,
moonshooter: moonshooter
}}));
};
setupGame (passing) {
if ((this.#state == 'complete') || (this.#state == 'passing')) return;
this.#passing = passing;
this.#hands = this.#dealCards();
this.#current_trick = null;
this.#collected_tricks = {
north: [],
east: [],
south: [],
west: []
};
this.#state = 'passing';
this.dispatchEvent(new Event('stateupdate'));
};
matchOver () {
this.#state = 'complete';
this.dispatchEvent(new Event('stateupdate'));
};
// These methods are available to view objects to query model information
getState () {
return this.#state;
}
getPlayerName (position) {
return this.#playerNames[position];
};
getCurrentGamePoints (position) {
return this.#collected_tricks[position].reduce((sum, trick) => sum + trick.getPoints(), 0);
};
getScoreLog () {
return this.#scorelog;
};
getScore (position) {
return this.#scorelog.reduce((score, entry) => score + entry[position], 0);
}
getPassing() {
return this.#passing;
}
getCurrentTrick () {
return this.#current_trick;
}
getTricksLeft () {
return 13 - HU.positions.reduce((sum, p) => sum += this.#collected_tricks[p].length, 0);
}
getHand (position) {
return this.#hands[position];
}
getCollectedTricks(position) {
return this.#collected_tricks[position];
}
}
export class Card {
#suit
#rank
static TWO_OF_CLUBS = new Card('clubs', 2);
static QUEEN_OF_SPADES = new Card('spades', 12);
constructor (suit, rank) {
this.#suit = suit;
this.#rank = rank;
}
toString() {
return `${this.getRankName()} of ${this.getSuit()}`;
}
getSuit() {
return this.#suit;
}
getRank() {
return this.#rank;
}
getRankName() {
let honors_map = {};
honors_map[11] = 'jack';
honors_map[12] = 'queen';
honors_map[13] = 'king';
honors_map[14] = 'ace';
return this.#rank < 11 ? this.#rank.toString() : honors_map[this.#rank];
}
equals(other) {
return (other.getRank() == this.#rank) && (other.getSuit() == this.#suit);
}
}
export class Trick {
#lead;
#next_to_play;
#played_by_position;
static #next_to_play_map = {
north: 'east',
east: 'south',
south: 'west',
west: 'north'
};
constructor(lead) {
this.#lead = lead;
this.#next_to_play = lead;
this.#played_by_position = {
north: null,
east: null,
south: null,
west: null
}
}
getLead() {
return this.#lead;
}
getLeadSuit() {
return this.getCard(this.getLead()).getSuit();
}
nextToPlay () {
return this.#next_to_play;
}
playCard (card) {
this.#played_by_position[this.#next_to_play] = card;
this.#next_to_play = this.isComplete() ? null : Trick.#next_to_play_map[this.#next_to_play];
}
getCard(position) {
return this.#played_by_position[position];
}
isComplete() {
return !HU.positions.find(p => this.#played_by_position[p] == null);
};
getPoints() {
return HU.positions.map(p => this.#played_by_position[p])
.filter(c => c != null)
.reduce((points, c) => points +
(c.equals(Card.QUEEN_OF_SPADES) ? 13 :
((c.getSuit() == 'hearts') ? 1 : 0)), 0);
}
toString() {
return `next to play: ${this.#next_to_play}
north: ${this.#played_by_position.north}
east : ${this.#played_by_position.east}
south: ${this.#played_by_position.south}
west : ${this.#played_by_position.west}
`
}
}
export class Hand extends EventTarget {
#cards;
constructor (cards) {
super();
this.#cards = cards;
}
contains (card) {
return this.#cards.some(c => c.equals(card));
}
hasSuit(suit) {
return this.#cards.some(c => c.getSuit() == suit);
}
hasOnlyHearts() {
return !this.#cards.some(c => c.getSuit() != 'hearts');
}
add (cards) {
if (cards.length == 0) return;
this.#cards.push(...cards);
this.dispatchEvent(new CustomEvent('update', {detail:
{type: 'add', cards: [...cards]}}));
}
remove (cards) {
if (cards.length == 0) return;
cards.forEach((c_to_remove) => {
this.#cards = this.#cards.filter((c) => !c.equals(c_to_remove));
});
this.dispatchEvent(new CustomEvent('update', {detail:
{type: 'remove', cards: [...cards]}}));
}
getCards () {
return [...this.#cards];
}
toString() {
return `Hearts : ${this.#cards.filter(c=> c.getSuit() == 'hearts').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()}
Spades : ${this.#cards.filter(c=> c.getSuit() == 'spades').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()}
Diamonds: ${this.#cards.filter(c=> c.getSuit() == 'diamonds').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()}
Clubs : ${this.#cards.filter(c=> c.getSuit() == 'clubs').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()}
`;
}
}
< /code>
Вот контроллер: < /p>
import { HU } from "./hearts_utils.js";
import { Card } from "./hearts_model.js";
export class HeartsController {
#model;
#cards_to_pass;
#hearts_broken;
constructor(model) {
this.#model = model;
this.#model.addEventListener('trickend', () => this.#handleTrickEnd());
}
#doAsync() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
startGame(north_name, east_name, south_name, west_name) {
this.#cards_to_pass = {
north: [],
east: [],
south: [],
west: []
};
this.#hearts_broken = false;
this.#doAsync().then(() => {
this.#model.initialize(north_name, east_name, south_name, west_name);
});
}
passCards(position, cards) {
if (this.#model.getState() != 'passing') {
alert('Controller error: attempt to pass cards when not in passing state');
return;
}
if (this.#model.getPassing() == 'none') {
alert('Controller error: attempt to pass cards when passing is none');
return;
}
if (cards.length != 3) {
alert('Controller error: attempt to pass more/less than three cards');
return;
}
let hand = this.#model.getHand(position);
if (cards.some(c => !hand.contains(c))) {
alert('Controller error: attempt to pass a card not in the hand of position');
return;
}
if (this.#cards_to_pass[position].length != 0) {
alert('Controller error: attempt to pass cards twice');
return;
}
this.#cards_to_pass[position] = [...cards];
if (!HU.positions.find(p => this.#cards_to_pass[p].length == 0)) {
this.#doAsync().then(() => {
this.#model.passCards(this.#cards_to_pass);
this.#cards_to_pass = {
north: [],
east: [],
south: [],
west: []
}
});
}
}
isPlayable(position, card) {
let cur_trick = this.#model.getCurrentTrick();
let hand = this.#model.getHand(position);
if (cur_trick.getLead() == position) {
// If lead of first trick in game, then only 2 of clubs is playable.
if (this.#model.getTricksLeft() == 13) {
return card.equals(Card.TWO_OF_CLUBS);
}
// Can only lead hearts if hearts are broken or hand only has hearts.
if (card.getSuit() == 'hearts') {
if (!this.#hearts_broken) {
return hand.hasOnlyHearts();
}
}
return true;
} else {
let lead_card = cur_trick.getCard(cur_trick.getLead());
if (!lead_card) {
return false;
}
if (!hand.hasSuit(lead_card.getSuit())) {
return true;
}
return card.getSuit() == lead_card.getSuit();
}
}
playCard(position, card) {
if (this.#model.getState() != 'playing') {
alert('Controller error: playCard called when not in playing state.');
return;
}
if (this.#model.getCurrentTrick().nextToPlay() != position) {
alert('Controller error: attempt to play card out of position');
return;
}
if (!this.#model.getHand(position).contains(card)) {
alert('Controller error: attmept to play card not in hand');
return;
}
if (!this.isPlayable(position, card)) {
alert('Controller error: attmept to play unplayable card');
return;
}
this.#doAsync().then(() => {
this.#model.playCardIntoTrick(position, card);
this.#hearts_broken ||= (card.getSuit() == 'hearts');
});
}
#handleTrickEnd() {
// Figure out who won.
let cur_trick = this.#model.getCurrentTrick();
let winner = cur_trick.getLead();
let winning_card = cur_trick.getCard(winner);
HU.positions.forEach(position => {
if (winner != position) {
let card = cur_trick.getCard(position);
if ((card.getSuit() == winning_card.getSuit()) &&
(card.getRank() > winning_card.getRank())) {
winning_card = card;
winner = position;
}
}
});
this.#doAsync().then(() => this.#model.collectTrick(winner))
.then(() => {
if (this.#model.getTricksLeft() > 0) {
this.#model.setupTrick(winner);
return false;
} else {
// Game's over.
// Create scorelog entry (detect shooting the moon)
// Update scorelog
// Detect possible match end.
// Figure out next passing mode and set up next game.
// let scorelog_entry = HU.positions.reduce((entry, pos) => entry[pos] = this.#model.getCurrentGamePoints(pos), {});
let scorelog_entry = {
north: this.#model.getCurrentGamePoints('north'),
east: this.#model.getCurrentGamePoints('east'),
south: this.#model.getCurrentGamePoints('south'),
west: this.#model.getCurrentGamePoints('west'),
};
let moonshooter = HU.positions.find(p => scorelog_entry[p] == 26);
if (moonshooter) {
HU.positions.forEach(p => {
scorelog_entry[p] = (scorelog_entry[p] + 26) % 52;
});
} else {
moonshooter = null;
}
this.#model.updateScoreLog(scorelog_entry, moonshooter);
return true;
}
})
.then((game_over) => {
if (game_over) {
if (HU.positions.find(p => this.#model.getScore(p) >= 100)) {
this.#model.matchOver();
} else {
let next_passing = HU.next_passing_map[this.#model.getPassing()];
this.#hearts_broken = false;
this.#cards_to_pass = {
north: [],
east: [],
south: [],
west: []
};
this.#model.setupGame(next_passing);
if (next_passing == 'none') {
this.#doAsync().then(() => this.#model.passCards(this.#cards_to_pass));
}
}
}
});
}
}
и вот представление (только файл, который может быть изменен)
Код: Выделить всё
import {HeartsRobotKmp} from "./hearts_robot_kmp.js";
import {Card, Hand, Trick} from "./hearts_model.js";
import {HU} from "./hearts_utils.js";
export class HeartsView {
#model
#controller
constructor(model, controller) {
this.#model = model;
this.#controller = controller;
this.dealPressed = false;
}
render(render_div) {
this.render_div = render_div;
render_div.innerHTML = '';
this.#model.addEventListener("stateupdate", () => this.update());
this.#model.addEventListener("trickplay", () => this.updateTrick());
this.#model.addEventListener("scoreupdate", () => this.updateScores());
if (this.#model.getState() === 'uninitialized') {
this.renderNameSetup();
} else {
this.renderGameTable();
this.update();
}
}
renderNameSetup() {
this.render_div.innerHTML = `
Welcome to Hearts!
Your Name:
West Player:
North Player:
East Player:
Start Game
`;
document.getElementById('confirm-names').addEventListener('click', () => {
const south = document.getElementById('player-name').value || "You";
const west = document.getElementById('west-name').value;
const north = document.getElementById('north-name').value;
const east = document.getElementById('east-name').value;
this.#model.getPlayerName('north');
this.#model.getPlayerName('east');
this.#model.getPlayerName('south');
this.#model.getPlayerName('west');
this.#controller.startGame(north, east, south, west);
});
}
renderGameTable() {
const names = {
north: this.#model.getPlayerName('north'),
east: this.#model.getPlayerName('east'),
south: this.#model.getPlayerName('south'),
west: this.#model.getPlayerName('west')
}
this.render_div.innerHTML = `
'north north north'
'west center east'
'south south south';
gap: 20px; justify-items: center; align-items: center;">
${names.north}
${names.west}
Trick Goes Here
Deal
Pass
Scores Go Here
${names.east}
${names.south}
`;
document.getElementById("deal-button").addEventListener("click", () => {
this.dealPressed = true;
this.renderHands();
});
}
update() {
const state = this.#model.getState();
if (!document.getElementById("game-table")) {
this.renderGameTable();
}
if (state === 'passing') {
if (this.dealPressed) {
this.showPassCardsUI();
}
} else if (state === 'playing') {
this.showPlayableCards();
this.updateTrick();
} else if (state === 'complete') {
this.showWinner();
}
this.updateScores();
}
renderHands() {
const positions = ["north", "east", "south", "west"];
positions.forEach(position => {
const hand = this.#model.getHand(position); // A Hand object
if (!hand) return;
const cards = hand.getCards(); // Array of Card objects
const container = document.getElementById(`${position}-hand`);
container.innerHTML = '';
cards.forEach(card => {
const cardDiv = document.createElement("div");
cardDiv.className = "card";
cardDiv.textContent = `${card.getRankName()} of ${card.getSuit()}`;
container.appendChild(cardDiv);
});
});
}
showPassCardsUI() {
const southHandDiv = document.getElementById('south-hand');
const passButton = document.getElementById('pass-button');
passButton.style.display = 'inline-block';
const selectedCards = new Set();
// Make each card clickable
[...southHandDiv.children].forEach(cardDiv => {
cardDiv.addEventListener('click', () => {
const cardText = cardDiv.textContent;
cardDiv.classList.toggle('selected');
if (selectedCards.has(cardText)) {
selectedCards.delete(cardText);
} else if (selectedCards.size < 3) {
selectedCards.add(cardText);
}
passButton.disabled = selectedCards.size !== 3;
});
});
passButton.disabled = true;
passButton.addEventListener('click', () => {
// Convert text labels back to actual Card objects from model
const southHand = this.#model.getHand('south').getCards();
const selectedCardObjs = [...selectedCards].map(text =>
southHand.find(c => `${c.getRankName()} of ${c.getSuit()}` === text)
);
// Player passes cards
this.#controller.passCard("south", selectedCardObjs);
// Other players pass random cards
for (const pos of ["north", "east", "west"]) {
const hand = this.#model.getHand(pos).getCards();
const randomCards = this.getRandomCards(hand, 3);
this.#controller.passCard(pos, randomCards);
}
passButton.style.display = 'none'; // Hide after passing
});
}
getRandomCards(cards, count) {
const shuffled = [...cards].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
}
showPlayableCards() { /* To be implemented */ }
updateTrick() { /* To be implemented */ }
updateScores() { /* To be implemented */ }
showWinner() { /* To be implemented */ }
renderMatchOver() {
// todo
}
}
Любая помощь будет оценена, большое спасибо!
Подробнее здесь:
https://stackoverflow.com/questions/796 ... ockwise-to
1746286176
Anonymous
Я пытаюсь выполнить задание, в котором я должен создать карт Game Game of Hearts. Кнопка «пройти». Тем не менее, я не могу заставить кнопку прохода появиться. Я также считаю, что должен поместить каждую карту в контейнер, чтобы сделать их кликабельными, но я не на 100%, как это реализовать. Я должен только обновлять представление, а не модель или контроллер. [b] Вот модель: [/b] [code]import {HU} from "./hearts_utils.js"; export class HeartsModel extends EventTarget { #playerNames; #state; #scorelog; #hands; #current_trick; #collected_tricks; #passing; constructor() { super(); this.#playerNames = { north: null, east: null, south: null, west: null }; this.#state = 'uninitialized'; this.#scorelog = []; this.#hands = { north: null, east: null, south: null, west: null }; this.#collected_tricks = { north: [], east: [], south: [], west: [] }; } // Private methods #dealCards () { let deck = []; ['spades', 'hearts', 'diamonds', 'clubs'].forEach(suit => { [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].forEach(rank => { deck.push(new Card(suit, rank)); }) }); deck = deck.map((card) => [card, Math.random()]).sort((a,b) => a[1]-b[1]).map((cpair) => cpair[0]); return { north: new Hand(deck.slice(0,13)), east: new Hand(deck.slice(13,26)), south: new Hand(deck.slice(26,39)), west: new Hand(deck.slice(39,52)) }; } // These methods are only available to controller to update model. // They should never be called from view objects. initialize (north_name, east_name, south_name, west_name) { if (this.#state != 'uninitialized') return; this.#playerNames.north = north_name; this.#playerNames.east = east_name; this.#playerNames.south = south_name; this.#playerNames.west = west_name; this.#scorelog = []; this.setupGame('right'); }; passCards (cards_to_pass) { if (this.#state != 'passing') return; HU.positions.forEach(p => this.#hands[p].remove(cards_to_pass[p])); HU.positions.forEach(p => { let pass_to = HU.passing_maps[this.#passing][p]; this.#hands[pass_to].add(cards_to_pass[p]); }); this.#state = 'playing'; let lead = HU.positions.find(p => this.#hands[p].contains(Card.TWO_OF_CLUBS)); this.#current_trick = new Trick(lead); this.dispatchEvent(new Event('stateupdate')); this.dispatchEvent(new Event('trickstart')); }; playCardIntoTrick (position, card) { if (this.#state != 'playing') return; this.#hands[position].remove([card]); this.#current_trick.playCard(card); this.dispatchEvent(new CustomEvent('trickplay', {detail: { position: position, card: card }})); if (this.#current_trick.isComplete()) { this.dispatchEvent(new Event('trickend')); } }; collectTrick (position) { if (this.#state != 'playing') return; this.#collected_tricks[position].push(this.#current_trick); this.dispatchEvent(new CustomEvent('trickcollected', {detail: { position: position, trick: this.#current_trick} })); this.#current_trick = null; }; setupTrick (position) { if (this.#state != 'playing') return; this.#current_trick = new Trick(position); this.dispatchEvent(new Event('trickstart')); }; updateScoreLog (scorelog_entry, moonshooter) { this.#scorelog.push(scorelog_entry); this.dispatchEvent(new CustomEvent('scoreupdate', {detail: { entry: scorelog_entry, moonshooter: moonshooter }})); }; setupGame (passing) { if ((this.#state == 'complete') || (this.#state == 'passing')) return; this.#passing = passing; this.#hands = this.#dealCards(); this.#current_trick = null; this.#collected_tricks = { north: [], east: [], south: [], west: [] }; this.#state = 'passing'; this.dispatchEvent(new Event('stateupdate')); }; matchOver () { this.#state = 'complete'; this.dispatchEvent(new Event('stateupdate')); }; // These methods are available to view objects to query model information getState () { return this.#state; } getPlayerName (position) { return this.#playerNames[position]; }; getCurrentGamePoints (position) { return this.#collected_tricks[position].reduce((sum, trick) => sum + trick.getPoints(), 0); }; getScoreLog () { return this.#scorelog; }; getScore (position) { return this.#scorelog.reduce((score, entry) => score + entry[position], 0); } getPassing() { return this.#passing; } getCurrentTrick () { return this.#current_trick; } getTricksLeft () { return 13 - HU.positions.reduce((sum, p) => sum += this.#collected_tricks[p].length, 0); } getHand (position) { return this.#hands[position]; } getCollectedTricks(position) { return this.#collected_tricks[position]; } } export class Card { #suit #rank static TWO_OF_CLUBS = new Card('clubs', 2); static QUEEN_OF_SPADES = new Card('spades', 12); constructor (suit, rank) { this.#suit = suit; this.#rank = rank; } toString() { return `${this.getRankName()} of ${this.getSuit()}`; } getSuit() { return this.#suit; } getRank() { return this.#rank; } getRankName() { let honors_map = {}; honors_map[11] = 'jack'; honors_map[12] = 'queen'; honors_map[13] = 'king'; honors_map[14] = 'ace'; return this.#rank < 11 ? this.#rank.toString() : honors_map[this.#rank]; } equals(other) { return (other.getRank() == this.#rank) && (other.getSuit() == this.#suit); } } export class Trick { #lead; #next_to_play; #played_by_position; static #next_to_play_map = { north: 'east', east: 'south', south: 'west', west: 'north' }; constructor(lead) { this.#lead = lead; this.#next_to_play = lead; this.#played_by_position = { north: null, east: null, south: null, west: null } } getLead() { return this.#lead; } getLeadSuit() { return this.getCard(this.getLead()).getSuit(); } nextToPlay () { return this.#next_to_play; } playCard (card) { this.#played_by_position[this.#next_to_play] = card; this.#next_to_play = this.isComplete() ? null : Trick.#next_to_play_map[this.#next_to_play]; } getCard(position) { return this.#played_by_position[position]; } isComplete() { return !HU.positions.find(p => this.#played_by_position[p] == null); }; getPoints() { return HU.positions.map(p => this.#played_by_position[p]) .filter(c => c != null) .reduce((points, c) => points + (c.equals(Card.QUEEN_OF_SPADES) ? 13 : ((c.getSuit() == 'hearts') ? 1 : 0)), 0); } toString() { return `next to play: ${this.#next_to_play} north: ${this.#played_by_position.north} east : ${this.#played_by_position.east} south: ${this.#played_by_position.south} west : ${this.#played_by_position.west} ` } } export class Hand extends EventTarget { #cards; constructor (cards) { super(); this.#cards = cards; } contains (card) { return this.#cards.some(c => c.equals(card)); } hasSuit(suit) { return this.#cards.some(c => c.getSuit() == suit); } hasOnlyHearts() { return !this.#cards.some(c => c.getSuit() != 'hearts'); } add (cards) { if (cards.length == 0) return; this.#cards.push(...cards); this.dispatchEvent(new CustomEvent('update', {detail: {type: 'add', cards: [...cards]}})); } remove (cards) { if (cards.length == 0) return; cards.forEach((c_to_remove) => { this.#cards = this.#cards.filter((c) => !c.equals(c_to_remove)); }); this.dispatchEvent(new CustomEvent('update', {detail: {type: 'remove', cards: [...cards]}})); } getCards () { return [...this.#cards]; } toString() { return `Hearts : ${this.#cards.filter(c=> c.getSuit() == 'hearts').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()} Spades : ${this.#cards.filter(c=> c.getSuit() == 'spades').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()} Diamonds: ${this.#cards.filter(c=> c.getSuit() == 'diamonds').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()} Clubs : ${this.#cards.filter(c=> c.getSuit() == 'clubs').sort((a,b) => a.getRank() - b.getRank()).map(c => c.getRankName()).join()} `; } } < /code> Вот контроллер: < /p> import { HU } from "./hearts_utils.js"; import { Card } from "./hearts_model.js"; export class HeartsController { #model; #cards_to_pass; #hearts_broken; constructor(model) { this.#model = model; this.#model.addEventListener('trickend', () => this.#handleTrickEnd()); } #doAsync() { return new Promise((resolve) => setTimeout(resolve, 0)); } startGame(north_name, east_name, south_name, west_name) { this.#cards_to_pass = { north: [], east: [], south: [], west: [] }; this.#hearts_broken = false; this.#doAsync().then(() => { this.#model.initialize(north_name, east_name, south_name, west_name); }); } passCards(position, cards) { if (this.#model.getState() != 'passing') { alert('Controller error: attempt to pass cards when not in passing state'); return; } if (this.#model.getPassing() == 'none') { alert('Controller error: attempt to pass cards when passing is none'); return; } if (cards.length != 3) { alert('Controller error: attempt to pass more/less than three cards'); return; } let hand = this.#model.getHand(position); if (cards.some(c => !hand.contains(c))) { alert('Controller error: attempt to pass a card not in the hand of position'); return; } if (this.#cards_to_pass[position].length != 0) { alert('Controller error: attempt to pass cards twice'); return; } this.#cards_to_pass[position] = [...cards]; if (!HU.positions.find(p => this.#cards_to_pass[p].length == 0)) { this.#doAsync().then(() => { this.#model.passCards(this.#cards_to_pass); this.#cards_to_pass = { north: [], east: [], south: [], west: [] } }); } } isPlayable(position, card) { let cur_trick = this.#model.getCurrentTrick(); let hand = this.#model.getHand(position); if (cur_trick.getLead() == position) { // If lead of first trick in game, then only 2 of clubs is playable. if (this.#model.getTricksLeft() == 13) { return card.equals(Card.TWO_OF_CLUBS); } // Can only lead hearts if hearts are broken or hand only has hearts. if (card.getSuit() == 'hearts') { if (!this.#hearts_broken) { return hand.hasOnlyHearts(); } } return true; } else { let lead_card = cur_trick.getCard(cur_trick.getLead()); if (!lead_card) { return false; } if (!hand.hasSuit(lead_card.getSuit())) { return true; } return card.getSuit() == lead_card.getSuit(); } } playCard(position, card) { if (this.#model.getState() != 'playing') { alert('Controller error: playCard called when not in playing state.'); return; } if (this.#model.getCurrentTrick().nextToPlay() != position) { alert('Controller error: attempt to play card out of position'); return; } if (!this.#model.getHand(position).contains(card)) { alert('Controller error: attmept to play card not in hand'); return; } if (!this.isPlayable(position, card)) { alert('Controller error: attmept to play unplayable card'); return; } this.#doAsync().then(() => { this.#model.playCardIntoTrick(position, card); this.#hearts_broken ||= (card.getSuit() == 'hearts'); }); } #handleTrickEnd() { // Figure out who won. let cur_trick = this.#model.getCurrentTrick(); let winner = cur_trick.getLead(); let winning_card = cur_trick.getCard(winner); HU.positions.forEach(position => { if (winner != position) { let card = cur_trick.getCard(position); if ((card.getSuit() == winning_card.getSuit()) && (card.getRank() > winning_card.getRank())) { winning_card = card; winner = position; } } }); this.#doAsync().then(() => this.#model.collectTrick(winner)) .then(() => { if (this.#model.getTricksLeft() > 0) { this.#model.setupTrick(winner); return false; } else { // Game's over. // Create scorelog entry (detect shooting the moon) // Update scorelog // Detect possible match end. // Figure out next passing mode and set up next game. // let scorelog_entry = HU.positions.reduce((entry, pos) => entry[pos] = this.#model.getCurrentGamePoints(pos), {}); let scorelog_entry = { north: this.#model.getCurrentGamePoints('north'), east: this.#model.getCurrentGamePoints('east'), south: this.#model.getCurrentGamePoints('south'), west: this.#model.getCurrentGamePoints('west'), }; let moonshooter = HU.positions.find(p => scorelog_entry[p] == 26); if (moonshooter) { HU.positions.forEach(p => { scorelog_entry[p] = (scorelog_entry[p] + 26) % 52; }); } else { moonshooter = null; } this.#model.updateScoreLog(scorelog_entry, moonshooter); return true; } }) .then((game_over) => { if (game_over) { if (HU.positions.find(p => this.#model.getScore(p) >= 100)) { this.#model.matchOver(); } else { let next_passing = HU.next_passing_map[this.#model.getPassing()]; this.#hearts_broken = false; this.#cards_to_pass = { north: [], east: [], south: [], west: [] }; this.#model.setupGame(next_passing); if (next_passing == 'none') { this.#doAsync().then(() => this.#model.passCards(this.#cards_to_pass)); } } } }); } [/code] } [b] и вот представление (только файл, который может быть изменен) [/b] [code]import {HeartsRobotKmp} from "./hearts_robot_kmp.js"; import {Card, Hand, Trick} from "./hearts_model.js"; import {HU} from "./hearts_utils.js"; export class HeartsView { #model #controller constructor(model, controller) { this.#model = model; this.#controller = controller; this.dealPressed = false; } render(render_div) { this.render_div = render_div; render_div.innerHTML = ''; this.#model.addEventListener("stateupdate", () => this.update()); this.#model.addEventListener("trickplay", () => this.updateTrick()); this.#model.addEventListener("scoreupdate", () => this.updateScores()); if (this.#model.getState() === 'uninitialized') { this.renderNameSetup(); } else { this.renderGameTable(); this.update(); } } renderNameSetup() { this.render_div.innerHTML = ` Welcome to Hearts! Your Name: West Player: North Player: East Player: Start Game `; document.getElementById('confirm-names').addEventListener('click', () => { const south = document.getElementById('player-name').value || "You"; const west = document.getElementById('west-name').value; const north = document.getElementById('north-name').value; const east = document.getElementById('east-name').value; this.#model.getPlayerName('north'); this.#model.getPlayerName('east'); this.#model.getPlayerName('south'); this.#model.getPlayerName('west'); this.#controller.startGame(north, east, south, west); }); } renderGameTable() { const names = { north: this.#model.getPlayerName('north'), east: this.#model.getPlayerName('east'), south: this.#model.getPlayerName('south'), west: this.#model.getPlayerName('west') } this.render_div.innerHTML = ` 'north north north' 'west center east' 'south south south'; gap: 20px; justify-items: center; align-items: center;"> ${names.north} ${names.west} Trick Goes Here Deal Pass Scores Go Here ${names.east} ${names.south} `; document.getElementById("deal-button").addEventListener("click", () => { this.dealPressed = true; this.renderHands(); }); } update() { const state = this.#model.getState(); if (!document.getElementById("game-table")) { this.renderGameTable(); } if (state === 'passing') { if (this.dealPressed) { this.showPassCardsUI(); } } else if (state === 'playing') { this.showPlayableCards(); this.updateTrick(); } else if (state === 'complete') { this.showWinner(); } this.updateScores(); } renderHands() { const positions = ["north", "east", "south", "west"]; positions.forEach(position => { const hand = this.#model.getHand(position); // A Hand object if (!hand) return; const cards = hand.getCards(); // Array of Card objects const container = document.getElementById(`${position}-hand`); container.innerHTML = ''; cards.forEach(card => { const cardDiv = document.createElement("div"); cardDiv.className = "card"; cardDiv.textContent = `${card.getRankName()} of ${card.getSuit()}`; container.appendChild(cardDiv); }); }); } showPassCardsUI() { const southHandDiv = document.getElementById('south-hand'); const passButton = document.getElementById('pass-button'); passButton.style.display = 'inline-block'; const selectedCards = new Set(); // Make each card clickable [...southHandDiv.children].forEach(cardDiv => { cardDiv.addEventListener('click', () => { const cardText = cardDiv.textContent; cardDiv.classList.toggle('selected'); if (selectedCards.has(cardText)) { selectedCards.delete(cardText); } else if (selectedCards.size < 3) { selectedCards.add(cardText); } passButton.disabled = selectedCards.size !== 3; }); }); passButton.disabled = true; passButton.addEventListener('click', () => { // Convert text labels back to actual Card objects from model const southHand = this.#model.getHand('south').getCards(); const selectedCardObjs = [...selectedCards].map(text => southHand.find(c => `${c.getRankName()} of ${c.getSuit()}` === text) ); // Player passes cards this.#controller.passCard("south", selectedCardObjs); // Other players pass random cards for (const pos of ["north", "east", "west"]) { const hand = this.#model.getHand(pos).getCards(); const randomCards = this.getRandomCards(hand, 3); this.#controller.passCard(pos, randomCards); } passButton.style.display = 'none'; // Hide after passing }); } getRandomCards(cards, count) { const shuffled = [...cards].sort(() => 0.5 - Math.random()); return shuffled.slice(0, count); } showPlayableCards() { /* To be implemented */ } updateTrick() { /* To be implemented */ } updateScores() { /* To be implemented */ } showWinner() { /* To be implemented */ } renderMatchOver() { // todo } [/code] } Любая помощь будет оценена, большое спасибо! Подробнее здесь: [url]https://stackoverflow.com/questions/79604847/how-can-make-the-player-and-bots-choose-cards-to-pass-to-the-player-clockwise-to[/url]