1. 发牌与洗牌
  2. 叫地主逻辑
  3. 出牌规则判断 (单张、对子、顺子、炸弹等)
  4. 简单的AI对手
  5. 游戏流程控制

您可以将以下代码保存为一个 .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>

如何使用

  1. 复制代码:将上面所有的代码复制到一个文本编辑器中(如 VS Code, Sublime Text, 或记事本)。
  2. 保存文件:将文件保存为 斗地主.html(或任何你喜欢的名字,但后缀必须是 .html)。
  3. 打开浏览器:用 Chrome, Firefox, Edge 等现代浏览器打开这个 .html 文件。
  4. 开始游戏:点击页面上的 "开始游戏" 按钮,游戏就会自动开始。

代码结构说明

  1. HTML (<body> 部分):

    • 定义了游戏的布局,包括三个玩家区域、中央出牌区、控制按钮和信息显示区。
    • 使用了 divspan 来组织内容,并为关键元素设置了 id,方便 JavaScript 操作。
  2. CSS (<style> 部分):

    • 负责页面的视觉呈现。
    • 设置了牌桌的绿色背景、牌的样式(大小、颜色、阴影)、选中状态的高亮效果、按钮的样式等。
    • 使用了 Flexbox 布局,使页面元素能够灵活地居中和排列。
  3. JavaScript (<script> 部分):

    • 核心数据结构:
      • createDeck(): 创建包含54张牌的数组。
      • gameState: 一个对象,存储了游戏的所有状态,如玩家手牌、当前玩家、上一手牌等。
    • 游戏流程控制:
      • startNewGame(): 初始化游戏,洗牌,发牌,随机选择地主,并开始游戏。
      • nextTurn(): 切换到下一个玩家,并触发AI出牌逻辑。
      • endGame(): 判断胜负并显示结果。
    • 核心逻辑:
      • getCardType(): 判断一手牌是什么牌型(单张、对子、顺子、炸弹等),这是斗地主规则的核心。
      • canBeat(): 判断当前选中的牌是否能打过上一手牌。
      • aiPlay(): 简单的AI逻辑,会尝试出能打过上一手牌的最小牌,否则选择不出。
    • 用户交互:
      • toggleCardSelection(): 当用户点击自己的牌时,切换牌的选中状态。
      • playSelectedCards()pass(): 处理用户的出牌和不出操作。
    • 渲染与更新:
      • renderAllHands(): 根据当前游戏状态,重新绘制所有玩家的手牌。
      • updatePlayerInfo()updateButtons(): 更新界面上的玩家信息和按钮状态。

可扩展和改进的地方

这份代码已经是一个功能完整的斗地主游戏,但还可以进行很多优化和扩展:

斗地主的网页页面源代码
(图片来源网络,侵删)
  1. 更智能的AI: 目前的AI非常简单,可以增加更复杂的策略,

    • 记忆已出的牌。
    • 根据手牌牌型(是否有炸弹、顺子)来决定出牌策略。
    • 在合适的时机出王炸或炸弹。
    • 叫地主逻辑可以更复杂(根据手牌强度)。
  2. 更多牌型: 可以实现更复杂的牌型,如连对、飞机带翅膀、四带二等。

  3. 更好的UI/UX:

    • 添加动画效果,如发牌动画、出牌动画。
    • 优化牌的布局,当手牌很多时能自动换行或缩小牌的显示。
    • 添加音效(如洗牌声、出牌声)。
    • 实现拖拽出牌。
  4. 多人在线对战: 将本地游戏升级为真正的在线游戏,需要后端服务器(如 Node.js + Socket.io)来处理玩家匹配和游戏状态同步。

希望这份源代码对您有帮助!