Missed-round credit uses Math.round(((players - 2) * 20) / players) for each scored round they missed.
${availableSavedPlayers.map(player =>
`
${escapeHtml(player)}
`
).join('')}
`;
}
function toggleMidGameDropdown() {
toggleDropdownPanel('player-select-panel', document.getElementById('midgame-select-panel'));
}
function selectMidGamePlayer(name) {
document.querySelectorAll('.player-select-panel.open').forEach(p => p.classList.remove('open'));
document.getElementById('midGamePlayerSelect').value = name;
document.getElementById('midgame-select-label').textContent = name;
document.querySelectorAll('#midgame-select-panel .player-select-option').forEach(opt => {
opt.classList.toggle('selected', opt.textContent === name);
});
}
function openGamePlayersModal() {
renderGamePlayersModal();
document.getElementById('gamePlayersModal').classList.add('active');
}
function addPlayerMidGame() {
const select = document.getElementById('midGamePlayerSelect');
const input = document.getElementById('midGamePlayerName');
const name = (input.value.trim() || select.value.trim());
if (!name) {
alert('Choose a saved player or type a new player name.');
return;
}
if (gameState.players.some(player => player.name.toLowerCase() === name.toLowerCase())) {
alert('That player is already part of this game.');
return;
}
if (getActivePlayerIndexes().length >= 11) {
alert('You already have 11 active players. Disable someone first.');
return;
}
const player = {
name,
score: 0,
rounds: [],
active: true,
previousPosition: gameState.players.length + 1
};
const playerIndex = gameState.players.push(player) - 1;
gameState.rounds.forEach((round, roundIndex) => {
if (round.scored) {
const credit = calculateMissedRoundCredit(getRoundParticipatingCount(round));
round.playerData[playerIndex] = createRoundPlayerData(false, {
score: credit,
absentReason: 'joined-late'
});
player.score += credit;
player.rounds.push({
round: roundIndex + 1,
bid: null,
gotSet: false,
score: credit,
joinedLate: true
});
} else {
round.playerData[playerIndex] = createRoundPlayerData(true);
}
});
const savedPlayers = getSavedPlayers();
if (!savedPlayers.includes(name)) {
savePlayers([...savedPlayers, name]);
loadSavedPlayers();
}
renderGame();
updatePlayerPositions();
autoSave();
renderGamePlayersModal();
alert(`${name} added with ${player.score} catch-up points.`);
}
function disablePlayerMidGame(playerName) {
const playerIndex = gameState.players.findIndex(player => player.name === playerName);
if (playerIndex < 0) return;
const activeIndexes = getActivePlayerIndexes();
if (activeIndexes.length <= 3) {
alert('You need at least 3 active players.');
return;
}
if (!confirm(`Disable ${playerName} for the rest of the game?`)) {
return;
}
const currentRound = gameState.rounds[gameState.currentRound];
const currentRoundData = currentRound?.playerData?.[playerIndex];
gameState.players[playerIndex].active = false;
if (currentRound && !currentRound.scored && currentRoundData?.participating) {
currentRound.playerData[playerIndex] = createRoundPlayerData(false, { absentReason: 'disabled' });
if (currentRound.dealerIndex === playerIndex) {
const remainingActive = gameState.players
.map((player, idx) => (currentRound.playerData[idx]?.participating ? idx : null))
.filter(idx => idx !== null);
currentRound.dealerIndex = remainingActive[0] ?? currentRound.dealerIndex;
}
}
renderGame();
updatePlayerPositions();
autoSave();
renderGamePlayersModal();
}
function openRulesModal() {
const content = document.getElementById('rulesContent');
content.innerHTML = `
🎯 Game Overview
Oh Hell is a trick-taking card game where players must bid exactly how many tricks they'll win. The total bids cannot equal the hand size (marked as ❌ EXACT).
📊 Scoring Formula
Your score for a round depends on whether you made your bid:
✅ Made Your Bid (Positive Bid > 0)
Score = 10 + (Bid²) + Confidence - Deferred - Tax
Example: Bid 3, Confidence MAX (10), No deferred, No tax