Big Two Card Game Score System (with Blockchain Integration)
Big Two (BigTwo)
For details, please refer to the Wikipedia entry for Big Two. I won’t go into its history here; it’s mainly a popular card game in Guangdong region that I love playing. I once lost up to 400-500 RMB in one night playing it.
Scoring Rules
We agree there is only one winner per round (winner takes all). All players aim to play all their cards as fast as possible. Once one player empties their hand, all others are losers. Their score is calculated based on the number of cards remaining in their hand. For example, if you have 3 cards left, you score -3 points. The total points lost by all losers are added together as the winner’s total score.
Score Multiplier for Remaining Cards When someone wins (empties their hand), losers with a large number of remaining cards receive penalty multipliers. In my usual games (low base points, e.g., 0.5 RMB per point), we use high multipliers:
- 13 cards remaining (no cards played): ×4 multiplier (13 × 4 = 52 points)
- 10–12 cards remaining: ×3 multiplier (e.g., 11 cards = 33 points)
- 8–9 cards remaining: ×2 multiplier (e.g., 9 cards = 18 points)
- Fewer than 8 cards remaining: No multiplier (standard scoring)
Multiplier for Holding 2s Losers with 2s left in their hand get an additional multiplier:
- 1 two = ×2
- 2 twos = ×4 Formula: 2ⁿ (n = number of 2s caught)
Example Calculation:
shellPlayer A: 10 cards left, 2 twos → 10 * 3 * (2 * 2) = 120 (loss) Player B: 9 cards left, 1 two → 9 * 2 * 2 = 36 (loss) Player C: 6 cards left, 0 twos → 6 * 1 * 1 = 6 (loss) Player D (winner): 120 + 36 + 6 = 162 (win)In this round: Player A loses 120 points, Player B loses 36, Player C loses 6, Player D wins 162. Convert to cash with your point value (e.g., 0.2 RMB/point: A loses ¥24, B loses ¥7.2, C loses ¥1.2, D wins ¥32.4).
Python + Excel Solution
I used to keep manual scores and always made mistakes at the end. Manual tracking also doesn’t let you view real-time running totals. So I built a Python-based scoring system with Excel storage. After each round, input remaining cards and 2s count; the system calculates round and total winnings/losses. Detailed round data is stored in Excel.
Excel Table Format
| Round | A Remain | A Remain 2 | B Remain | B Remain 2 | C Remain | C Remain 2 | D Remain | D Remain 2 |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 3 | 1 | 2 | 0 | 4 | 1 |
| 2 | 3 | 0 | 7 | 2 | 9 | 1 | 0 | 0 |
This is a simple flat-table structure (not recommended for real databases like PostgreSQL/MySQL—you’ll get criticized by maintainers!). But it works for Excel.
Python Scripts
# config.py
# Fixed configuration variables
# Excel file path
EXCEL_FILE_PATH = "./play-2d.xlsx"
# Cash value per point (CNY)
CARD_PRICE = 0.5
# Player config (custom names, match Excel column order)
PLAYERS_CONFIG = {
"A": "APlayer",
"B": "BPlayer",
"C": "CPlayer",
"D": "DPlayer"
}
# Card count multiplier rules (high → low priority)
CARD_MULT_RULES = [
(range(13, 14), 4), # 13 cards → ×4
(range(10, 13), 3), # 10-12 cards → ×3
(range(8, 10), 2), # 8-9 cards → ×2
(range(0, 8), 1), # 0-7 cards → ×1
]
"""
Get multiplier based on remaining cards
:param remain_cards: Number of remaining cards
:return: Corresponding multiplier
"""
def get_card_mult(remain_cards: int) -> int:
# Return first matching rule
for condition, mult in CARD_MULT_RULES:
if remain_cards in condition:
return mult
return 1 # Fallback
# Precompute multiplier for 1-13 cards
CARD_MULT = { element: get_card_mult(element) for element in range(1, 14) }
# Rule config (all multipliers customizable)
RULES_CONFIG = {
"max_2_cards": 4, # Total 2s in a deck
"max_cards_per_player": 13, # Initial cards per player
"power_base": 2, # Base for 2s multiplier (2^n)
"card_mult": CARD_MULT,
}# main.py
# Main calculation script
import pandas as pd
from config import PLAYERS_CONFIG, RULES_CONFIG, EXCEL_FILE_PATH, CARD_PRICE
def calculate_single_loss(remain_cards, remain_2, rules):
if remain_cards == 0:
return 0.0
power_mult = rules["power_base"] ** remain_2
card_mult = rules["card_mult"][remain_cards]
total_mult = power_mult * card_mult
return remain_cards * total_mult * CARD_PRICE
def safe_convert_to_int(value, desc, round_num):
try:
return int(value), ""
except (ValueError, TypeError):
return None, f"Round {round_num} {desc} is not a valid number"
# ===================== Data Validation =====================
"""
Validate single round data (dynamic player support)
:param row: Row data
:param round_num: Round number
:param players: Player config
:param rules: Rule config
:return: (is_valid, error_msg, player_data)
"""
def validate_round_data(row, round_num, players, rules):
errors = []
player_codes = list(players.keys())
player_data = {}
col_idx = 0
for code in player_codes:
player_name = players[code]
cards_val = row.iloc[col_idx]
cards, err = safe_convert_to_int(cards_val, f"{player_name} remaining cards", round_num)
if err:
errors.append(err)
col_idx += 2
continue
remain_2_val = row.iloc[col_idx + 1]
remain_2, err2 = safe_convert_to_int(remain_2_val, f"{player_name} 2s count", round_num)
if err2:
errors.append(err2)
col_idx += 2
continue
player_data[code] = {
"name": player_name,
"cards": cards,
"remain_2": remain_2
}
col_idx += 2
if errors:
return False, " | ".join(errors), None
# Validate 2s count ≤ remaining cards
for code, data in player_data.items():
name = data["name"]
cards = data["cards"]
remain_2 = data["remain_2"]
if remain_2 > cards:
errors.append(f"{name} 2s count({remain_2}) > remaining cards({cards}) — invalid")
# Total 2s validation
total_remain_2 = sum([data["remain_2"] for data in player_data.values()])
if total_remain_2 < 0 or total_remain_2 > rules["max_2_cards"]:
errors.append(f"Total 2s = {total_remain_2} (out of range: 0~{rules['max_2_cards']})")
# Card count range validation
max_cards = rules["max_cards_per_player"]
for code, data in player_data.items():
name = data["name"]
cards = data["cards"]
if cards < 0 or cards > max_cards:
errors.append(f"{name} remaining cards({cards}) out of range (0~{max_cards})")
# Winner validation (exactly 1)
winner_count = sum([1 for data in player_data.values() if data["cards"] == 0])
if winner_count == 0:
errors.append("No winner (no player with 0 cards) — 1 winner required per round")
elif winner_count > 1:
errors.append(f"{winner_count} winners — only 1 allowed per round")
if errors:
error_msg = f"Round {round_num} data error: " + " | ".join(errors)
return False, error_msg, None
return True, "", player_data
def main():
try:
df = pd.read_excel(EXCEL_FILE_PATH, header=0)
print(f"✅ Excel loaded successfully (Players: {PLAYERS_CONFIG})")
print("=" * 80)
except FileNotFoundError:
print(f"❌ File not found: {EXCEL_FILE_PATH}")
return
except Exception as e:
print(f"❌ File read error: {e}")
return
# Initialize total scores
total_scores = {name: 0.0 for name in PLAYERS_CONFIG.values()}
for index, row in df.iterrows():
round_num = index + 1
# Validate column count
required_cols = len(PLAYERS_CONFIG) * 2
if len(row) < required_cols:
print(f"❌ Round {round_num}: Insufficient columns (need {required_cols}, got {len(row)})")
print("⚠️ Calculation stopped!")
return
# Validate round data
is_valid, error_msg, player_data = validate_round_data(
row, round_num, PLAYERS_CONFIG, RULES_CONFIG
)
if not is_valid:
print(f"❌ {error_msg}")
print("⚠️ Calculation stopped!")
return
round_losses = {}
total_round_loss = 0.0
winner_name = None
# Calculate losses
for _, data in player_data.items():
name = data["name"]
cards = data["cards"]
remain_2 = data["remain_2"]
loss = calculate_single_loss(cards, remain_2, RULES_CONFIG)
round_losses[name] = loss
total_round_loss += loss
if cards == 0:
winner_name = name
# Print round results
print(f"📌 Round {round_num} | Winner: {winner_name}")
for name in PLAYERS_CONFIG.values():
if name == winner_name:
win_money = total_round_loss
total_scores[name] += win_money
print(f" {name}: Won ¥{win_money:.2f}")
else:
lose_money = round_losses[name]
total_scores[name] -= lose_money
print(f" {name}: Lost ¥{lose_money:.2f}")
# Print running totals
print(" --- Running Total ---")
for name, score in total_scores.items():
if score > 0:
print(f" {name}: Total won ¥{score:.2f}")
elif score < 0:
print(f" {name}: Total lost ¥{abs(score):.2f}")
else:
print(f" {name}: No net win/loss")
print("-" * 80)
# Final results
print("\n🏆 Final Results")
print("=" * 80)
for name, score in total_scores.items():
if score > 0:
print(f"{name}: Total win ¥{score:.2f}")
elif score < 0:
print(f"{name}: Total loss ¥{abs(score):.2f}")
else:
print(f"{name}: No net win/loss")
if __name__ == "__main__":
main()Product Summary
This code fully supports a Big Two score calculator. Currently, it runs via command-line interface (CLI). It can be extended to a GUI/web app for better usability:
- Build a web app with Flask/Tornado (add UI + config editor for
config.py) - Port to mobile with Flutter/Jetpack Compose/SwiftUI (Dart/Kotlin/Swift) to impress friends
Technical Improvements
For web/mobile apps, replace Excel with SQLite (easier CRUD APIs than Excel). You can keep the flat-table structure or normalize the database for better design.
Blockchain Integration
Why Blockchain?
The local Excel solution works perfectly. Blockchain is added for learning/technical curiosity—not for necessity. If friends don’t trust you to edit Excel, treat the script as a calculator and confirm round totals manually.
Blockchain guarantees immutability (scores can’t be altered/deleted). This project is a hands-on way to learn blockchain, not just memorize theory.
Language: Solidity
Solidity is a curly-brace language for the Ethereum Virtual Machine (EVM), influenced by C++, Python, and JavaScript. It’s statically typed and supports inheritance, libraries, and custom types. Used for voting, crowdfunding, auctions, and multi-signature wallets.
1. Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
contract ChuDiDiScore {
uint256 public constant CARD_PRICE = 1;
uint256 public constant POWER_BASE = 2;
uint256 public constant MAX_2_CARDS = 4;
uint256 public constant MAX_CARDS = 13;
address[4] public players;
string[4] public playerNames;
mapping(address => int256) public totalScores;
struct Round {
uint256 roundId;
address winner;
int256[4] scores;
uint256[4] remainCards;
uint256[4] remain2;
}
Round[] public rounds;
uint256 public roundCount;
function createRoom(address[4] calldata _players, string[4] calldata _names) public {
for (uint256 i = 0; i < 4; i++) {
players[i] = _players[i];
playerNames[i] = _names[i];
}
roundCount = 0;
}
function _calculateLoss(uint256 remainCards, uint256 remain2) internal pure returns (uint256) {
if (remainCards == 0) return 0;
uint256 power_mult = remain2 == 0 ? 1 : (uint256(2) ** remain2);
uint256 card_mult = 1;
if (remainCards == 13) {
card_mult = 4;
} else if (remainCards >= 10 && remainCards <= 12) {
card_mult = 3;
} else if (remainCards >= 8 && remainCards <= 9) {
card_mult = 2;
} else {
card_mult = 1;
}
return remainCards * power_mult * card_mult * 1;
}
function submitRound(uint256[4] calldata _remainCards, uint256[4] calldata _remain2) public {
require(players[0] != address(0));
uint256 winnerCount = 0;
address winner;
uint256 totalRemain2 = 0;
for (uint256 i = 0; i < 4; i++) {
uint256 c = _remainCards[i];
uint256 r = _remain2[i];
require(c <= 13);
require(r <= c);
totalRemain2 += r;
if (c == 0) {
winnerCount++;
winner = players[i];
}
}
require(winnerCount == 1);
require(totalRemain2 <= 4);
int256[4] memory roundScores;
uint256 totalWin = 0;
for (uint256 i = 0; i < 4; i++) {
if (_remainCards[i] == 0) continue;
uint256 loss = _calculateLoss(_remainCards[i], _remain2[i]);
roundScores[i] = -int256(loss);
totalWin += loss;
}
for (uint256 i = 0; i < 4; i++) {
if (players[i] == winner) {
roundScores[i] = int256(totalWin);
break;
}
}
for (uint256 i = 0; i < 4; i++) {
totalScores[players[i]] += roundScores[i];
}
rounds.push(Round({
roundId: roundCount + 1,
winner: winner,
scores: roundScores,
remainCards: _remainCards,
remain2: _remain2
}));
roundCount++;
}
}2. Deploy Contract
Use Remix (online IDE) to compile/deploy the Solidity contract. Follow tutorials like Liao Xuefeng’s blog for step-by-step Remix usage.
3. Interact with Contract
- MetaMask: Ethereum wallet for interacting with public chain contracts (requires gas fees).
- Local Geth Node: For private, feeless testing (deploy via Docker Geth).
Geth provides an immutable, append-only ledger (no edits/deletions). Connect it to a backend (e.g., Flask) for APIs.
4. Python Flask Backend API
from flask import Flask, request, jsonify
from web3 import Web3
# Connect to local Geth private chain
w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
# Contract ABI (from your contract)
ABI = [
{
"inputs": [
{"internalType": "address[4]", "name": "_players", "type": "address[4]"},
{"internalType": "string[4]", "name": "_names", "type": "string[4]"}
],
"name": "createRoom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "uint256[4]", "name": "_remainCards", "type": "uint256[4]"},
{"internalType": "uint256[4]", "name": "_remain2", "type": "uint256[4]"}
],
"name": "submitRound",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{"inputs": [], "name": "CARD_PRICE", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "MAX_2_CARDS", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "MAX_CARDS", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "name": "playerNames", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "name": "players", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "POWER_BASE", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [], "name": "roundCount", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "name": "rounds", "outputs": [{"internalType": "uint256", "name": "roundId", "type": "uint256"},{"internalType": "address", "name": "winner", "type": "address"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "address", "name": "", "type": "address"}], "name": "totalScores", "outputs": [{"internalType": "int256", "name": "", "type": "int256"}], "stateMutability": "view", "type": "function"}
]
# Deployed contract address
CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
contract = w3.eth.contract(address=CONTRACT_ADDRESS, abi=ABI)
# Default account
account = w3.eth.accounts[0]
# ==========================================
# Flask API Service
# ==========================================
app = Flask(__name__)
# --------------------
# 1. Create game room (4 players)
# --------------------
@app.route('/createRoom', methods=['POST'])
def create_room():
try:
data = request.json
names = data.get("names", ["Player1", "Player2", "Player3", "Player4"])
players = w3.eth.accounts[:4]
tx_hash = contract.functions.createRoom(players, names).transact({
"from": account
})
w3.eth.wait_for_transaction_receipt(tx_hash)
return jsonify({
"code": 0,
"msg": "✅ Room created (on-chain)",
"players": players,
"names": names
})
except Exception as e:
return jsonify({"code": -1, "error": str(e)})
# --------------------
# 2. Submit round scores
# --------------------
@app.route('/submitRound', methods=['POST'])
def submit_round():
try:
data = request.json
remainCards = data["remainCards"]
remain2 = data["remain2"]
tx_hash = contract.functions.submitRound(remainCards, remain2).transact({
"from": account
})
w3.eth.wait_for_transaction_receipt(tx_hash)
return jsonify({
"code": 0,
"msg": "✅ Round recorded (immutable on-chain)"
})
except Exception as e:
return jsonify({"code": -1, "error": str(e)})
# --------------------
# 3. Get all player totals
# --------------------
@app.route('/scores', methods=['GET'])
def get_scores():
try:
result = []
for i in range(4):
addr = contract.functions.players(i).call()
name = contract.functions.playerNames(i).call()
score = contract.functions.totalScores(addr).call()
result.append({
"index": i,
"address": addr,
"name": name,
"score": int(score)
})
return jsonify({
"code": 0,
"scores": result
})
except Exception as e:
return jsonify({"code": -1, "error": str(e)})
# --------------------
# 4. Get total rounds
# --------------------
@app.route('/roundCount', methods=['GET'])
def round_count():
cnt = contract.functions.roundCount().call()
return jsonify({
"code": 0,
"roundCount": int(cnt)
})
# --------------------
# Health check
# --------------------
@app.route('/', methods=['GET'])
def index():
return jsonify({
"name": "Big Two Blockchain Score API",
"status": "running",
"chain": "Geth private"
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)