一、应用概述

1.1 应用简介

石头剪刀布(Rock Paper Scissors)是一款经典的人机猜拳游戏,玩家与电脑进行三选一的博弈,系统通过随机算法和策略模式决定电脑的出拳,同时提供完整的胜率统计、连胜记录、历史对局回顾和视觉反馈系统。该应用以HarmonyOS NEXT的ArkTS框架为基础,深入展示了游戏逻辑判定、概率算法、统计分析、策略模式、状态管理和视觉反馈等关键技术。

1.2 核心功能

功能模块 功能描述 技术实现 设计考量
基础游戏 玩家与电脑猜拳 胜负判定逻辑 三局两胜支持
电脑AI 电脑出拳策略 多种策略模式 可调节难度
胜率统计 胜/负/平统计 数据聚合 百分比可视化
连胜记录 连胜/最佳连胜 计数器 排行榜激励
历史对局 完整对局记录 列表渲染 带时间戳
视觉反馈 胜负动画效果 状态驱动动画 即时反馈
策略模式 随机/学习/循环 策略模式设计 增加趣味性

1.3 应用架构

石头剪刀布应用采用分层架构:

  1. UI表现层:出拳选择按钮、结果展示区、比分面板、历史记录列表。
  2. 业务逻辑层:游戏引擎、AI策略引擎、统计管理器、历史记录管理器。
  3. 数据持久层:使用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 核心技术要点

  1. 胜负判定算法:使用模运算公式 (player - computer + 3) % 3 实现简洁高效的判定。
  2. 策略模式架构:通过策略模式实现多种AI出拳策略,包括随机、循环、学习和频率模式。
  3. 学习型AI:分析玩家最近出拳频率,选择克制策略,增加游戏难度和趣味性。
  4. 完整统计系统:跟踪胜率、连胜、出拳偏好等多维度数据,提供数据可视化。
  5. 视觉反馈系统:使用动画和颜色变化提供即时的结果反馈。

8.2 扩展方向

  1. 多人模式:支持本地多人轮流对战。
  2. 手势识别:使用摄像头识别真实手势。
  3. 在线对战:通过网络与远程玩家对战。
  4. 锦标赛模式:多轮淘汰赛制。
  5. 自定义规则:支持添加新手势(如石头剪刀布蜥蜴史波克)。

8.3 核心代码量统计

模块 核心代码行数 接口数 组件数
游戏引擎 80 5 -
AI策略系统 160 6 -
统计管理器 150 6 -
UI组件 300 4 6
测试用例 60 - -
总计 750 21 6

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐