- 发牌与洗牌
- 叫地主逻辑
- 出牌规则判断 (单张、对子、顺子、炸弹等)
- 简单的AI对手
- 游戏流程控制
您可以将以下代码保存为一个 .html 文件,然后用浏览器打开即可直接运行。

(图片来源网络,侵删)
斗地主网页游戏源代码 (HTML + CSS + JavaScript)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">斗地主 - 网页版</title>
<style>
/* --- 全局样式 --- */
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #2C5F2D; /* 绿色背景,模拟牌桌 */
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
color: white;
min-height: 100vh;
}
h1 {
text-align: center;
color: #FFD700;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
#game-container {
width: 100%;
max-width: 1200px;
display: flex;
flex-direction: column;
align-items: center;
}
/* --- 玩家区域 --- */
.player-area {
width: 100%;
margin: 10px 0;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.player-info {
background: rgba(0, 0, 0, 0.6);
padding: 10px 20px;
border-radius: 10px;
margin: 0 10px;
text-align: center;
}
.player-info.landlord {
border: 2px solid #FFD700;
box-shadow: 0 0 10px #FFD700;
}
.player-name {
font-weight: bold;
margin-bottom: 5px;
}
.card-count {
font-size: 0.9em;
color: #ccc;
}
/* --- 牌的样式 --- */
.card {
width: 60px;
height: 90px;
background: white;
border: 1px solid #333;
border-radius: 5px;
margin: 0 -20px; /* 用于重叠效果 */
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 1.5em;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease-in-out;
position: relative;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.card:hover {
transform: translateY(-10px);
z-index: 10;
}
.card.selected {
transform: translateY(-20px);
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.7);
border: 2px solid #FFD700;
}
.card.red {
color: red;
}
.card.black {
color: black;
}
.card.joker {
background: linear-gradient(45deg, #FFD700, #FFA500);
color: #333;
}
/* --- 中央区域 --- */
#center-area {
width: 100%;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
#last-played-cards {
display: flex;
justify-content: center;
align-items: center;
min-height: 100px;
}
#landlord-cards {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
}
.landlord-card {
width: 50px;
height: 75px;
background: #4A4A4A;
border: 1px solid #333;
border-radius: 5px;
margin: 0 -15px;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: bold;
}
/* --- 控制按钮 --- */
#controls {
margin-top: 20px;
display: flex;
gap: 15px;
}
button {
padding: 10px 20px;
font-size: 1em;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
#play-cards-btn {
background: #4CAF50;
color: white;
}
#play-cards-btn:hover:not(:disabled) {
background: #45a049;
}
#play-cards-btn:disabled {
background: #cccccc;
cursor: not-allowed;
}
#pass-btn {
background: #f44336;
color: white;
}
#pass-btn:hover:not(:disabled) {
background: #da190b;
}
#pass-btn:disabled {
background: #cccccc;
cursor: not-allowed;
}
#start-game-btn {
background: #2196F3;
color: white;
}
#start-game-btn:hover {
background: #0b7dda;
}
/* --- 游戏信息 --- */
#game-info {
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.6);
border-radius: 10px;
text-align: center;
min-width: 300px;
}
.message {
font-size: 1.2em;
color: #FFD700;
}
</style>
</head>
<body>
<h1>斗地主</h1>
<div id="game-container">
<!-- 上方玩家 (AI 1) -->
<div class="player-area" id="player2-area">
<div class="player-info" id="player2-info">
<div class="player-name">电脑玩家 1</div>
<div class="card-count">剩余: <span id="player2-count">17</span> 张</div>
</div>
</div>
<!-- 中央区域 -->
<div id="center-area">
<div id="landlord-cards"></div>
<div id="last-played-cards"></div>
</div>
<!-- 下方玩家 (用户) -->
<div class="player-area" id="player0-area">
<div class="player-info" id="player0-info">
<div class="player-name">我</div>
<div class="card-count">剩余: <span id="player0-count">17</span> 张</div>
</div>
</div>
<!-- 右侧玩家 (AI 2) -->
<div class="player-area" id="player1-area">
<div class="player-info" id="player1-info">
<div class="player-name">电脑玩家 2</div>
<div class="card-count">剩余: <span id="player1-count">17</span> 张</div>
</div>
</div>
<!-- 控制按钮 -->
<div id="controls">
<button id="start-game-btn">开始游戏</button>
<button id="play-cards-btn" disabled>出牌</button>
<button id="pass-btn" disabled>不出</button>
</div>
<!-- 游戏信息 -->
<div id="game-info">
<div class="message" id="message">点击 "开始游戏" 开始新一局</div>
</div>
</div>
<script>
// --- 游戏核心逻辑 ---
// 牌的定义
const SUITS = ['♠', '♥', '♣', '♦'];
const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2'];
const JOKERS = ['小王', '大王'];
// 游戏状态
let gameState = {
players: [[], [], []], // 三个玩家的手牌
landlordIndex: -1, // 地主索引
currentPlayerIndex: 0, // 当前出牌玩家索引
lastPlay: null, // 上一手牌 { playerIndex, cards, type }
selectedCards: [], // 用户选中的牌
gameStarted: false,
passCount: 0 // 连续出牌次数,用于判断是否可以出牌
};
// DOM 元素
const startBtn = document.getElementById('start-game-btn');
const playCardsBtn = document.getElementById('play-cards-btn');
const passBtn = document.getElementById('pass-btn');
const messageEl = document.getElementById('message');
const lastPlayedCardsEl = document.getElementById('last-played-cards');
const landlordCardsEl = document.getElementById('landlord-cards');
// 事件监听
startBtn.addEventListener('click', startNewGame);
playCardsBtn.addEventListener('click', playSelectedCards);
passBtn.addEventListener('click', pass);
// 创建一副牌
function createDeck() {
let deck = [];
for (let suit of SUITS) {
for (let rank of RANKS) {
deck.push({ suit, rank, value: RANKS.indexOf(rank) + 3, display: rank + suit });
}
}
deck.push({ suit: '', rank: '小王', value: 16, display: '小王', isJoker: true });
deck.push({ suit: '', rank: '大王', value: 17, display: '大王', isJoker: true });
return deck;
}
// 洗牌
function shuffle(deck) {
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}
// 开始新游戏
function startNewGame() {
// 重置游戏状态
gameState = {
players: [[], [], []],
landlordIndex: -1,
currentPlayerIndex: 0,
lastPlay: null,
selectedCards: [],
gameStarted: true,
passCount: 0
};
// 清空界面
lastPlayedCardsEl.innerHTML = '';
landlordCardsEl.innerHTML = '';
messageEl.textContent = '游戏开始,正在发牌...';
updatePlayerInfo();
updateButtons();
// 创建并洗牌
let deck = shuffle(createDeck());
// 发牌,每人17张,留3张底牌
for (let i = 0; i < 51; i++) {
gameState.players[i % 3].push(deck[i]);
}
const landlordCards = deck.slice(51);
// 排序手牌
gameState.players.forEach(hand => sortCards(hand));
// 简单叫地主逻辑 (随机)
gameState.landlordIndex = Math.floor(Math.random() * 3);
gameState.players[gameState.landlordIndex].push(...landlordCards);
sortCards(gameState.players[gameState.landlordIndex]);
// 渲染牌
renderAllHands();
updatePlayerInfo();
// 地主先出牌
gameState.currentPlayerIndex = gameState.landlordIndex;
messageEl.textContent = `电脑玩家 ${gameState.landlordIndex + 1} 是地主,请出牌`;
// 如果地主是AI,自动出牌
if (gameState.currentPlayerIndex !== 0) {
setTimeout(aiPlay, 1500);
} else {
updateButtons();
}
}
// 排序手牌
function sortCards(hand) {
hand.sort((a, b) => a.value - b.value);
}
// 渲染所有玩家的手牌
function renderAllHands() {
// 渲染玩家0 (用户) 的手牌
const player0HandEl = document.getElementById('player0-area');
let player0CardsHtml = '';
gameState.players[0].forEach((card, index) => {
const colorClass = card.isJoker ? 'joker' : (card.suit === '♥' || card.suit === '♦' ? 'red' : 'black');
player0CardsHtml += `<div class="card ${colorClass}" data-index="${index}" onclick="toggleCardSelection(${index})">${card.display}</div>`;
});
player0HandEl.innerHTML = `<div class="player-info" id="player0-info"><div class="player-name">我</div><div class="card-count">剩余: <span id="player0-count">${gameState.players[0].length}</span> 张</div></div>` + player0CardsHtml;
// 渲染AI玩家的手牌 (背面)
for (let i = 1; i < 3; i++) {
const playerHandEl = document.getElementById(`player${i}-area`);
let aiCardsHtml = '';
for (let j = 0; j < gameState.players[i].length; j++) {
aiCardsHtml += `<div class="card" style="background: #4A4A4A; color: white;">🂠</div>`;
}
playerHandEl.innerHTML = `<div class="player-info" id="player${i}-info"><div class="player-name">电脑玩家 ${i}</div><div class="card-count">剩余: <span id="player${i}-count">${gameState.players[i].length}</span> 张</div></div>` + aiCardsHtml;
}
// 显示地主标记
document.querySelectorAll('.player-info').forEach(el => el.classList.remove('landlord'));
if (gameState.landlordIndex !== -1) {
document.getElementById(`player${gameState.landlordIndex}-info`).classList.add('landlord');
}
}
// 切换牌的选中状态
function toggleCardSelection(index) {
if (gameState.currentPlayerIndex !== 0 || !gameState.gameStarted) return;
const cardEl = document.querySelector(`#player0-area .card[data-index="${index}"]`);
const card = gameState.players[0][index];
const selectedIndex = gameState.selectedCards.findIndex(c => c === card);
if (selectedIndex > -1) {
gameState.selectedCards.splice(selectedIndex, 1);
cardEl.classList.remove('selected');
} else {
gameState.selectedCards.push(card);
cardEl.classList.add('selected');
}
updateButtons();
}
// 更新按钮状态
function updateButtons() {
const isPlayerTurn = gameState.currentPlayerIndex === 0;
const canPass = gameState.lastPlay && gameState.lastPlay.playerIndex !== 0;
playCardsBtn.disabled = !isPlayerTurn || gameState.selectedCards.length === 0;
passBtn.disabled = !isPlayerTurn || !canPass;
}
// 更新玩家信息
function updatePlayerInfo() {
for (let i = 0; i < 3; i++) {
document.getElementById(`player${i}-count`).textContent = gameState.players[i].length;
}
}
// 判断牌型
function getCardType(cards) {
const len = cards.length;
if (len === 0) return null;
// 单张
if (len === 1) return { type: 'SINGLE', value: cards[0].value };
// 对子
if (len === 2 && cards[0].value === cards[1].value) {
return { type: 'PAIR', value: cards[0].value };
}
// 王炸
if (len === 2 && cards[0].value === 16 && cards[1].value === 17) {
return { type: 'ROCKET', value: 100 };
}
// 炸弹
if (len === 4 && cards[0].value === cards[1].value && cards[1].value === cards[2].value && cards[2].value === cards[3].value) {
return { type: 'BOMB', value: cards[0].value };
}
// 顺子 (5张或以上)
if (len >= 5) {
let isSequence = true;
for (let i = 0; i < len - 1; i++) {
if (cards[i].value !== cards[i + 1].value - 1 || cards[i].value >= 15) { // 2和王不能在顺子里
isSequence = false;
break;
}
}
if (isSequence) {
return { type: 'STRAIGHT', value: cards[len - 1].value, length: len };
}
}
// 三带一 / 三带二
if (len === 4 || len === 5) {
const counts = {};
cards.forEach(card => counts[card.value] = (counts[card.value] || 0) + 1);
const values = Object.keys(counts).map(Number).sort((a, b) => b - a);
if (values.length === 2) {
if ((counts[values[0]] === 3 && counts[values[1]] === 1) || (counts[values[0]] === 3 && counts[values[1]] === 2)) {
return { type: 'THREE_WITH_ONE', value: values[0] };
}
}
}
// 飞机 (TODO: 可以扩展更多牌型)
if (len >= 6 && len % 3 === 0) {
let isPlane = true;
for (let i = 0; i < len; i += 3) {
if (cards[i].value !== cards[i+1].value || cards[i+1].value !== cards[i+2].value || cards[i].value >= 15) {
isPlane = false;
break;
}
if (i > 0 && cards[i].value !== cards[i-3].value - 1) {
isPlane = false;
break;
}
}
if (isPlane) {
return { type: 'PLANE', value: cards[len-1].value, length: len / 3 };
}
}
return null; // 无效牌型
}
// 比较两手牌
function canBeat(newCards, lastCards) {
const newType = getCardType(newCards);
const lastType = getCardType(lastCards);
if (!newType || !lastType) return false;
// 王炸最大
if (newType.type === 'ROCKET') return true;
if (lastType.type === 'ROCKET') return false;
// 炸弹可以炸任何非炸弹牌型
if (newType.type === 'BOMB' && lastType.type !== 'BOMB') return true;
if (newType.type !== 'BOMB' && lastType.type === 'BOMB') return false;
// 同类型比较
if (newType.type === lastType.type) {
// 顺子、飞机需要长度相同
if ((newType.type === 'STRAIGHT' || newType.type === 'PLANE') && newType.length !== lastType.length) {
return false;
}
return newType.value > lastType.value;
}
return false;
}
// 出牌
function playSelectedCards() {
const cardsToPlay = [...gameState.selectedCards];
const cardType = getCardType(cardsToPlay);
if (!cardType) {
messageEl.textContent = '无效的牌型,请重新选择!';
return;
}
// 如果有上一手牌,需要判断是否能打过
if (gameState.lastPlay && gameState.lastPlay.playerIndex !== 0) {
if (!canBeat(cardsToPlay, gameState.lastPlay.cards)) {
messageEl.textContent = '出牌必须大于上一手!';
return;
}
}
// 出牌成功
makePlay(0, cardsToPlay);
}
// 不出
function pass() {
if (gameState.lastPlay && gameState.lastPlay.playerIndex !== 0) {
messageEl.textContent = '你选择不出';
nextTurn();
}
}
// 执行出牌动作
function makePlay(playerIndex, cards) {
// 从手牌中移除
cards.forEach(card => {
const index = gameState.players[playerIndex].findIndex(c => c.value === card.value && c.rank === card.rank);
if (index > -1) {
gameState.players[playerIndex].splice(index, 1);
}
});
// 更新游戏状态
gameState.lastPlay = { playerIndex, cards, type: getCardType(cards) };
gameState.selectedCards = [];
gameState.passCount = 0;
// 显示出的牌
displayLastPlayedCards(cards);
// 检查是否获胜
if (gameState.players[playerIndex].length === 0) {
endGame(playerIndex);
return;
}
// 更新界面
renderAllHands();
updatePlayerInfo();
updateButtons();
messageEl.textContent = `玩家 ${playerIndex === 0 ? '你' : `电脑玩家 ${playerIndex}`} 出了 ${cards.length} 张牌`;
// 下一回合
nextTurn();
}
// 显示上一手牌
function displayLastPlayedCards(cards) {
lastPlayedCardsEl.innerHTML = '';
cards.forEach(card => {
const colorClass = card.isJoker ? 'joker' : (card.suit === '♥' || card.suit === '♦' ? 'red' : 'black');
const cardEl = document.createElement('div');
cardEl.className = `card ${colorClass}`;
cardEl.textContent = card.display;
lastPlayedCardsEl.appendChild(cardEl);
});
}
// 下一回合
function nextTurn() {
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % 3;
// 如果一轮下来所有人都pass了,清空桌面
if (gameState.passCount >= 2) {
gameState.lastPlay = null;
gameState.passCount = 0;
lastPlayedCardsEl.innerHTML = '';
messageEl.textContent = '一轮结束,无人出牌,新的一轮开始';
}
if (gameState.currentPlayerIndex !== 0) {
setTimeout(aiPlay, 1500);
} else {
messageEl.textContent = '轮到你了,请出牌';
updateButtons();
}
}
// AI 出牌逻辑 (简单版)
function aiPlay() {
const aiHand = gameState.players[gameState.currentPlayerIndex];
let cardsToPlay = null;
// 如果是第一个出牌或者上一轮都pass了,AI出最小的单张
if (!gameState.lastPlay || gameState.passCount >= 2) {
cardsToPlay = [aiHand[0]];
} else {
// 尝试找到能打过的牌
const lastCards = gameState.lastPlay.cards;
const lastType = gameState.lastPlay.type;
// 简单策略:只尝试出同类型且更大的牌
if (lastType.type === 'SINGLE') {
for (let card of aiHand) {
if (card.value > lastType.value) {
cardsToPlay = [card];
break;
}
}
} else if (lastType.type === 'PAIR') {
for (let i = 0; i < aiHand.length - 1; i++) {
if (aiHand[i].value === aiHand[i + 1].value && aiHand[i].value > lastType.value) {
cardsToPlay = [aiHand[i], aiHand[i + 1]];
break;
}
}
}
// TODO: 可以扩展更多AI策略,如出炸弹、顺子等
}
if (cardsToPlay) {
makePlay(gameState.currentPlayerIndex, cardsToPlay);
} else {
// 不出
gameState.passCount++;
messageEl.textContent = `电脑玩家 ${gameState.currentPlayerIndex + 1} 选择不出`;
nextTurn();
}
}
// 游戏结束
function endGame(winnerIndex) {
gameState.gameStarted = false;
const isLandlordWin = winnerIndex === gameState.landlordIndex;
const winnerName = winnerIndex === 0 ? '你' : `电脑玩家 ${winnerIndex}`;
const result = isLandlordWin ? '地主' : '农民';
messageEl.textContent = `游戏结束!${winnerName}(${result})获胜!`;
updateButtons();
renderAllHands(); // 重新渲染,让玩家看到最终手牌
}
</script>
</body>
</html>
如何使用
- 复制代码:将上面所有的代码复制到一个文本编辑器中(如 VS Code, Sublime Text, 或记事本)。
- 保存文件:将文件保存为
斗地主.html(或任何你喜欢的名字,但后缀必须是.html)。 - 打开浏览器:用 Chrome, Firefox, Edge 等现代浏览器打开这个
.html文件。 - 开始游戏:点击页面上的 "开始游戏" 按钮,游戏就会自动开始。
代码结构说明
-
HTML (
<body>部分):- 定义了游戏的布局,包括三个玩家区域、中央出牌区、控制按钮和信息显示区。
- 使用了
div和span来组织内容,并为关键元素设置了id,方便 JavaScript 操作。
-
CSS (
<style>部分):- 负责页面的视觉呈现。
- 设置了牌桌的绿色背景、牌的样式(大小、颜色、阴影)、选中状态的高亮效果、按钮的样式等。
- 使用了 Flexbox 布局,使页面元素能够灵活地居中和排列。
-
JavaScript (
<script>部分):- 核心数据结构:
createDeck(): 创建包含54张牌的数组。gameState: 一个对象,存储了游戏的所有状态,如玩家手牌、当前玩家、上一手牌等。
- 游戏流程控制:
startNewGame(): 初始化游戏,洗牌,发牌,随机选择地主,并开始游戏。nextTurn(): 切换到下一个玩家,并触发AI出牌逻辑。endGame(): 判断胜负并显示结果。
- 核心逻辑:
getCardType(): 判断一手牌是什么牌型(单张、对子、顺子、炸弹等),这是斗地主规则的核心。canBeat(): 判断当前选中的牌是否能打过上一手牌。aiPlay(): 简单的AI逻辑,会尝试出能打过上一手牌的最小牌,否则选择不出。
- 用户交互:
toggleCardSelection(): 当用户点击自己的牌时,切换牌的选中状态。playSelectedCards()和pass(): 处理用户的出牌和不出操作。
- 渲染与更新:
renderAllHands(): 根据当前游戏状态,重新绘制所有玩家的手牌。updatePlayerInfo()和updateButtons(): 更新界面上的玩家信息和按钮状态。
- 核心数据结构:
可扩展和改进的地方
这份代码已经是一个功能完整的斗地主游戏,但还可以进行很多优化和扩展:

(图片来源网络,侵删)
-
更智能的AI: 目前的AI非常简单,可以增加更复杂的策略,
- 记忆已出的牌。
- 根据手牌牌型(是否有炸弹、顺子)来决定出牌策略。
- 在合适的时机出王炸或炸弹。
- 叫地主逻辑可以更复杂(根据手牌强度)。
-
更多牌型: 可以实现更复杂的牌型,如连对、飞机带翅膀、四带二等。
-
更好的UI/UX:
- 添加动画效果,如发牌动画、出牌动画。
- 优化牌的布局,当手牌很多时能自动换行或缩小牌的显示。
- 添加音效(如洗牌声、出牌声)。
- 实现拖拽出牌。
-
多人在线对战: 将本地游戏升级为真正的在线游戏,需要后端服务器(如 Node.js + Socket.io)来处理玩家匹配和游戏状态同步。
希望这份源代码对您有帮助!
