# ✊ 石头剪刀布 — 鸿蒙ArkTS游戏概率与统计系统设计
·
一、应用概述
1.1 应用简介
石头剪刀布(Rock Paper Scissors)是一款经典的人机猜拳游戏,玩家与电脑进行三选一的博弈,系统通过随机算法和策略模式决定电脑的出拳,同时提供完整的胜率统计、连胜记录、历史对局回顾和视觉反馈系统。该应用以HarmonyOS NEXT的ArkTS框架为基础,深入展示了游戏逻辑判定、概率算法、统计分析、策略模式、状态管理和视觉反馈等关键技术。
1.2 核心功能
| 功能模块 | 功能描述 | 技术实现 | 设计考量 |
|---|---|---|---|
| 基础游戏 | 玩家与电脑猜拳 | 胜负判定逻辑 | 三局两胜支持 |
| 电脑AI | 电脑出拳策略 | 多种策略模式 | 可调节难度 |
| 胜率统计 | 胜/负/平统计 | 数据聚合 | 百分比可视化 |
| 连胜记录 | 连胜/最佳连胜 | 计数器 | 排行榜激励 |
| 历史对局 | 完整对局记录 | 列表渲染 | 带时间戳 |
| 视觉反馈 | 胜负动画效果 | 状态驱动动画 | 即时反馈 |
| 策略模式 | 随机/学习/循环 | 策略模式设计 | 增加趣味性 |
1.3 应用架构
石头剪刀布应用采用分层架构:
- UI表现层:出拳选择按钮、结果展示区、比分面板、历史记录列表。
- 业务逻辑层:游戏引擎、AI策略引擎、统计管理器、历史记录管理器。
- 数据持久层:使用Preferences API保存统计数据和对局历史。
二、游戏逻辑
2.1 胜负判定
enum Choice {
ROCK = 0, // 石头
SCISSORS = 1, // 剪刀
PAPER = 2 // 布
}
enum GameResult {
WIN = 'win',
LOSE = 'lose',
DRAW = 'draw'
}
interface RoundResult {
playerChoice: Choice;
computerChoice: Choice;
result: GameResult;
playerScore: number;
computerScore: number;
timestamp: number;
roundNumber: number;
}
class RockPaperScissorsEngine {
private static readonly CHOICE_NAMES: Record<Choice, string> = {
[Choice.ROCK]: '✊ 石头',
[Choice.SCISSORS]: '✌️ 剪刀',
[Choice.PAPER]: '✋ 布'
};
private static readonly CHOICE_EMOJIS: Record<Choice, string> = {
[Choice.ROCK]: '✊',
[Choice.SCISSORS]: '✌️',
[Choice.PAPER]: '✋'
};
// 胜负判定规则
// 石头(0) 胜 剪刀(1), 剪刀(1) 胜 布(2), 布(2) 胜 石头(0)
// 判定公式: (player - computer + 3) % 3
// 结果为 1: 玩家胜, 2: 电脑胜, 0: 平局
determineWinner(player: Choice, computer: Choice): GameResult {
if (player === computer) return GameResult.DRAW;
const diff = (player - computer + 3) % 3;
if (diff === 1) return GameResult.WIN;
return GameResult.LOSE;
}
getChoiceName(choice: Choice): string {
return RockPaperScissorsEngine.CHOICE_NAMES[choice];
}
getChoiceEmoji(choice: Choice): string {
return RockPaperScissorsEngine.CHOICE_EMOJIS[choice];
}
// 生成结果描述
getResultDescription(result: RoundResult): string {
const playerChoice = this.getChoiceName(result.playerChoice);
const computerChoice = this.getChoiceName(result.computerChoice);
switch (result.result) {
case GameResult.WIN:
return `${playerChoice} 胜 ${computerChoice},你赢了!`;
case GameResult.LOSE:
return `${playerChoice} 负 ${computerChoice},你输了!`;
case GameResult.DRAW:
return `${playerChoice} 平 ${computerChoice},平局!`;
}
}
}
三、AI策略系统
3.1 策略模式设计
为了增加游戏的趣味性和挑战性,我们设计了多种AI策略,使用策略模式进行灵活切换:
interface AIStrategy {
name: string;
description: string;
getChoice(history: RoundResult[]): Choice;
}
// 策略1:完全随机
class RandomStrategy implements AIStrategy {
name = '随机模式';
description = '完全随机出拳,适合新手';
getChoice(history: RoundResult[]): Choice {
return Math.floor(Math.random() * 3) as Choice;
}
}
// 策略2:循环模式(石头→剪刀→布→石头...)
class CycleStrategy implements AIStrategy {
name = '循环模式';
description = '按固定顺序循环出拳';
private lastChoice: Choice = Choice.ROCK;
getChoice(history: RoundResult[]): Choice {
this.lastChoice = ((this.lastChoice + 1) % 3) as Choice;
return this.lastChoice;
}
}
// 策略3:学习模式(根据玩家偏好出拳)
class LearningStrategy implements AIStrategy {
name = '学习模式';
description = '分析玩家出拳习惯,选择克制策略';
private choiceCounts: Record<Choice, number> = {
[Choice.ROCK]: 0,
[Choice.SCISSORS]: 0,
[Choice.PAPER]: 0
};
getChoice(history: RoundResult[]): Choice {
// 统计玩家最近10次出拳
const recentHistory = history.slice(-10);
this.choiceCounts = { [Choice.ROCK]: 0, [Choice.SCISSORS]: 0, [Choice.PAPER]: 0 };
for (const round of recentHistory) {
this.choiceCounts[round.playerChoice]++;
}
// 找出玩家最常出的拳
let maxCount = 0;
let mostFrequent = Choice.ROCK;
for (const [choice, count] of Object.entries(this.choiceCounts)) {
if (count > maxCount) {
maxCount = count;
mostFrequent = parseInt(choice) as Choice;
}
}
// 出克制玩家最常出拳的拳
// 石头(0) 被 布(2) 克制, 剪刀(1) 被 石头(0) 克制, 布(2) 被 剪刀(1) 克制
const counterMap: Record<Choice, Choice> = {
[Choice.ROCK]: Choice.PAPER,
[Choice.SCISSORS]: Choice.ROCK,
[Choice.PAPER]: Choice.SCISSORS
};
return counterMap[mostFrequent];
}
}
// 策略4:基于频率的随机(根据玩家历史出拳分布)
class FrequencyStrategy implements AIStrategy {
name = '频率模式';
description = '基于玩家出拳频率分布进行随机';
getChoice(history: RoundResult[]): Choice {
if (history.length < 5) {
return Math.floor(Math.random() * 3) as Choice;
}
const recentHistory = history.slice(-20);
const counts = { [Choice.ROCK]: 0, [Choice.SCISSORS]: 0, [Choice.PAPER]: 0 };
for (const round of recentHistory) {
counts[round.playerChoice]++;
}
// 根据玩家出拳频率,选择克制概率最高的出拳
const total = recentHistory.length;
const probabilities = {
[Choice.ROCK]: counts[Choice.SCISSORS] / total, // 玩家出剪刀多,电脑出石头
[Choice.SCISSORS]: counts[Choice.PAPER] / total, // 玩家出布多,电脑出剪刀
[Choice.PAPER]: counts[Choice.ROCK] / total // 玩家出石头多,电脑出布
};
// 按概率随机选择
const rand = Math.random();
let cumulative = 0;
for (const [choice, prob] of Object.entries(probabilities)) {
cumulative += prob;
if (rand <= cumulative) {
return parseInt(choice) as Choice;
}
}
return Math.floor(Math.random() * 3) as Choice;
}
}
3.2 AI策略管理器
class AIStrategyManager {
private strategies: Map<string, AIStrategy> = new Map();
private currentStrategy: AIStrategy;
private history: RoundResult[] = [];
constructor() {
this.registerStrategy('random', new RandomStrategy());
this.registerStrategy('cycle', new CycleStrategy());
this.registerStrategy('learning', new LearningStrategy());
this.registerStrategy('frequency', new FrequencyStrategy());
this.currentStrategy = this.strategies.get('random')!;
}
registerStrategy(id: string, strategy: AIStrategy): void {
this.strategies.set(id, strategy);
}
setStrategy(id: string): boolean {
const strategy = this.strategies.get(id);
if (strategy) {
this.currentStrategy = strategy;
return true;
}
return false;
}
getChoice(): Choice {
return this.currentStrategy.getChoice(this.history);
}
recordRound(result: RoundResult): void {
this.history.push(result);
}
getCurrentStrategy(): AIStrategy {
return this.currentStrategy;
}
getAllStrategies(): { id: string; strategy: AIStrategy }[] {
return Array.from(this.strategies.entries()).map(([id, strategy]) => ({
id, strategy
}));
}
}
四、统计系统
4.1 统计数据模型
interface GameStatistics {
totalGames: number;
wins: number;
losses: number;
draws: number;
winRate: number;
currentStreak: number;
bestStreak: number;
roundHistory: RoundResult[];
choiceStats: Record<Choice, ChoiceStat>;
lastPlayDate: string;
}
interface ChoiceStat {
used: number;
wins: number;
losses: number;
draws: number;
winRate: number;
}
class StatisticsManager {
private stats: GameStatistics = this.getDefaultStats();
private prefs: preferences.Preferences | null = null;
private getDefaultStats(): GameStatistics {
return {
totalGames: 0, wins: 0, losses: 0, draws: 0,
winRate: 0, currentStreak: 0, bestStreak: 0,
roundHistory: [],
choiceStats: {
[Choice.ROCK]: { used: 0, wins: 0, losses: 0, draws: 0, winRate: 0 },
[Choice.SCISSORS]: { used: 0, wins: 0, losses: 0, draws: 0, winRate: 0 },
[Choice.PAPER]: { used: 0, wins: 0, losses: 0, draws: 0, winRate: 0 }
},
lastPlayDate: ''
};
}
recordGame(round: RoundResult): void {
this.stats.totalGames++;
this.stats.lastPlayDate = new Date().toISOString().split('T')[0];
// 更新胜负
switch (round.result) {
case GameResult.WIN:
this.stats.wins++;
this.stats.currentStreak++;
this.stats.bestStreak = Math.max(this.stats.bestStreak, this.stats.currentStreak);
break;
case GameResult.LOSE:
this.stats.losses++;
this.stats.currentStreak = 0;
break;
case GameResult.DRAW:
this.stats.draws++;
break;
}
// 更新胜率
this.stats.winRate = this.stats.totalGames > 0
? Math.round(this.stats.wins / this.stats.totalGames * 100)
: 0;
// 更新出拳统计
const playerChoice = round.playerChoice;
this.stats.choiceStats[playerChoice].used++;
switch (round.result) {
case GameResult.WIN:
this.stats.choiceStats[playerChoice].wins++;
break;
case GameResult.LOSE:
this.stats.choiceStats[playerChoice].losses++;
break;
case GameResult.DRAW:
this.stats.choiceStats[playerChoice].draws++;
break;
}
// 更新胜率
const cs = this.stats.choiceStats[playerChoice];
cs.winRate = cs.used > 0 ? Math.round(cs.wins / cs.used * 100) : 0;
// 保存历史记录
this.stats.roundHistory.push(round);
if (this.stats.roundHistory.length > 100) {
this.stats.roundHistory.shift();
}
this.save();
}
getStatistics(): GameStatistics {
return { ...this.stats };
}
// 获取最佳出拳
getBestChoice(): { choice: Choice; winRate: number } {
let bestChoice = Choice.ROCK;
let bestRate = 0;
for (const [choice, stat] of Object.entries(this.stats.choiceStats)) {
if (stat.used >= 3 && stat.winRate > bestRate) {
bestRate = stat.winRate;
bestChoice = parseInt(choice) as Choice;
}
}
return { choice: bestChoice, winRate: bestRate };
}
async init(context: Context): Promise<void> {
this.prefs = await preferences.getPreferences(context, 'rps_prefs');
await this.load();
}
private async load(): Promise<void> {
if (!this.prefs) return;
const json = await this.prefs.get('rps_statistics', '');
if (json) {
try { this.stats = JSON.parse(json); } catch { this.stats = this.getDefaultStats(); }
}
}
private async save(): Promise<void> {
if (!this.prefs) return;
await this.prefs.put('rps_statistics', JSON.stringify(this.stats));
await this.prefs.flush();
}
}
五、UI交互设计
5.1 游戏主界面
@Component
struct RockPaperScissorsView {
@State playerChoice: Choice | null = null;
@State computerChoice: Choice | null = null;
@State result: GameResult | null = null;
@State playerScore: number = 0;
@State computerScore: number = 0;
@State streak: number = 0;
@State showResult: boolean = false;
@State currentStrategy: string = '随机模式';
@State roundHistory: RoundResult[] = [];
private engine = new RockPaperScissorsEngine();
private aiManager = new AIStrategyManager();
private statsManager = new StatisticsManager();
build() {
Column() {
// 标题
Text('石头剪刀布')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 16, bottom: 8 })
// 比分板
this.renderScoreBoard()
// 策略选择器
this.renderStrategySelector()
// 对战区域
this.renderBattleArea()
// 出拳选择按钮
this.renderChoiceButtons()
// 结果展示
if (this.showResult) {
this.renderResultDisplay()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
private renderScoreBoard() {
Row() {
Column() {
Text('👤 玩家')
.fontSize(14)
.fontColor('#666666')
Text(`${this.playerScore}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Text('VS')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF9800')
Column() {
Text('🤖 电脑')
.fontSize(14)
.fontColor('#666666')
Text(`${this.computerScore}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#E53935')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin(16)
}
@Builder
private renderBattleArea() {
Row() {
// 玩家出拳
Column() {
Text(this.playerChoice !== null ? this.engine.getChoiceEmoji(this.playerChoice) : '❓')
.fontSize(64)
Text('你的出拳')
.fontSize(12)
.fontColor('#666666')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
// 对战结果
if (this.result) {
Text(this.getResultEmoji())
.fontSize(48)
}
// 电脑出拳
Column() {
Text(this.computerChoice !== null ? this.engine.getChoiceEmoji(this.computerChoice) : '❓')
.fontSize(64)
Text('电脑出拳')
.fontSize(12)
.fontColor('#666666')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin(16)
}
@Builder
private renderChoiceButtons() {
Row({ space: 16 }) {
this.createChoiceButton(Choice.ROCK, '✊', '石头')
this.createChoiceButton(Choice.SCISSORS, '✌️', '剪刀')
this.createChoiceButton(Choice.PAPER, '✋', '布')
}
.padding(16)
.justifyContent(FlexAlign.Center)
}
@Builder
private createChoiceButton(choice: Choice, emoji: string, label: string) {
Column() {
Text(emoji)
.fontSize(48)
Text(label)
.fontSize(12)
.fontColor('#666666')
}
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.shadow({ radius: 4, color: '#20000000', offsetY: 2 })
.onClick(() => {
this.playRound(choice);
})
}
private playRound(playerChoice: Choice): void {
const computerChoice = this.aiManager.getChoice();
const result = this.engine.determineWinner(playerChoice, computerChoice);
this.playerChoice = playerChoice;
this.computerChoice = computerChoice;
this.result = result;
this.showResult = true;
const roundResult: RoundResult = {
playerChoice, computerChoice, result,
playerScore: this.playerScore,
computerScore: this.computerScore,
timestamp: Date.now(),
roundNumber: this.roundHistory.length + 1
};
this.aiManager.recordRound(roundResult);
this.roundHistory.push(roundResult);
if (result === GameResult.WIN) {
this.playerScore++;
this.streak++;
} else if (result === GameResult.LOSE) {
this.computerScore++;
this.streak = 0;
}
this.statsManager.recordGame(roundResult);
}
private getResultEmoji(): string {
switch (this.result) {
case GameResult.WIN: return '🎉';
case GameResult.LOSE: return '😢';
case GameResult.DRAW: return '🤝';
}
}
}
六、视觉反馈系统
6.1 结果动画
@Component
struct ResultAnimation {
@Link result: GameResult | null;
@State opacity: number = 0;
@State scale: number = 0.5;
aboutToAppear(): void {
this.animate();
}
animate(): void {
animateTo({
duration: 300,
curve: Curve.EaseOut
}, () => {
this.opacity = 1;
this.scale = 1;
});
}
build() {
if (!this.result) return;
Text(this.getResultText())
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(this.getResultColor())
.opacity(this.opacity)
.scale({ x: this.scale, y: this.scale })
}
private getResultText(): string {
switch (this.result) {
case GameResult.WIN: return '🎉 你赢了!';
case GameResult.LOSE: return '😢 你输了!';
case GameResult.DRAW: return '🤝 平局!';
}
}
private getResultColor(): string {
switch (this.result) {
case GameResult.WIN: return '#4CAF50';
case GameResult.LOSE: return '#E53935';
case GameResult.DRAW: return '#FF9800';
}
}
}
七、测试策略
7.1 单元测试
describe('RockPaperScissorsEngine', () => {
const engine = new RockPaperScissorsEngine();
it('should determine rock beats scissors', () => {
expect(engine.determineWinner(Choice.ROCK, Choice.SCISSORS)).toBe(GameResult.WIN);
});
it('should determine scissors beats paper', () => {
expect(engine.determineWinner(Choice.SCISSORS, Choice.PAPER)).toBe(GameResult.WIN);
});
it('should determine paper beats rock', () => {
expect(engine.determineWinner(Choice.PAPER, Choice.ROCK)).toBe(GameResult.WIN);
});
it('should detect draw', () => {
expect(engine.determineWinner(Choice.ROCK, Choice.ROCK)).toBe(GameResult.DRAW);
expect(engine.determineWinner(Choice.SCISSORS, Choice.SCISSORS)).toBe(GameResult.DRAW);
expect(engine.determineWinner(Choice.PAPER, Choice.PAPER)).toBe(GameResult.DRAW);
});
it('should detect loss', () => {
expect(engine.determineWinner(Choice.ROCK, Choice.PAPER)).toBe(GameResult.LOSE);
expect(engine.determineWinner(Choice.SCISSORS, Choice.ROCK)).toBe(GameResult.LOSE);
expect(engine.determineWinner(Choice.PAPER, Choice.SCISSORS)).toBe(GameResult.LOSE);
});
});
describe('LearningStrategy', () => {
it('should counter player most frequent choice', () => {
const strategy = new LearningStrategy();
const history = [
{ playerChoice: Choice.ROCK, computerChoice: Choice.SCISSORS, result: GameResult.WIN },
{ playerChoice: Choice.ROCK, computerChoice: Choice.PAPER, result: GameResult.LOSE },
{ playerChoice: Choice.ROCK, computerChoice: Choice.SCISSORS, result: GameResult.WIN },
];
const choice = strategy.getChoice(history as any);
expect(choice).toBe(Choice.PAPER); // 应该出布来克制石头
});
});
八、总结
8.1 核心技术要点
- 胜负判定算法:使用模运算公式
(player - computer + 3) % 3实现简洁高效的判定。 - 策略模式架构:通过策略模式实现多种AI出拳策略,包括随机、循环、学习和频率模式。
- 学习型AI:分析玩家最近出拳频率,选择克制策略,增加游戏难度和趣味性。
- 完整统计系统:跟踪胜率、连胜、出拳偏好等多维度数据,提供数据可视化。
- 视觉反馈系统:使用动画和颜色变化提供即时的结果反馈。
8.2 扩展方向
- 多人模式:支持本地多人轮流对战。
- 手势识别:使用摄像头识别真实手势。
- 在线对战:通过网络与远程玩家对战。
- 锦标赛模式:多轮淘汰赛制。
- 自定义规则:支持添加新手势(如石头剪刀布蜥蜴史波克)。
8.3 核心代码量统计
| 模块 | 核心代码行数 | 接口数 | 组件数 |
|---|---|---|---|
| 游戏引擎 | 80 | 5 | - |
| AI策略系统 | 160 | 6 | - |
| 统计管理器 | 150 | 6 | - |
| UI组件 | 300 | 4 | 6 |
| 测试用例 | 60 | - | - |
| 总计 | 750 | 21 | 6 |
更多推荐



所有评论(0)