Rock-Paper-Scissors Game
Interactive Rock-Paper-Scissors Game
Section titled “Interactive Rock-Paper-Scissors Game”Introduction
Section titled “Introduction”This example demonstrates a Rock-Paper-Scissors game state machine that manages the game flow, tracks scores, and determines winners. It shows how state machines can handle complex conditional logic and state transitions in an interactive application.
Source Code
Section titled “Source Code”import { defineStates } from "matchina";
export type Move = "rock" | "paper" | "scissors";
export const states = defineStates({ WaitingForPlayer: (playerScore: number = 0, computerScore: number = 0) => ({ playerScore, computerScore, }), PlayerChose: ( playerMove: Move, playerScore: number, computerScore: number ) => ({ playerMove, playerScore, computerScore, }), Judging: ( playerMove: Move, computerMove: Move, playerScore: number, computerScore: number ) => ({ playerMove, computerMove, playerScore, computerScore, }), RoundComplete: ( playerMove: Move, computerMove: Move, roundWinner: "player" | "computer" | "tie", playerScore: number, computerScore: number ) => ({ playerMove, computerMove, roundWinner, playerScore, computerScore, }), GameOver: ( winner: "player" | "computer", playerScore: number, computerScore: number ) => ({ winner, playerScore, computerScore, }),});
import { createMachine, assignEventApi } from "matchina";import { states, type Move } from "./states";import { randomMove, determineWinner } from "./game-utils";
export function createRPSMachine() { const machine = createMachine( states, { WaitingForPlayer: { selectMove: (move: Move) => ({ from }) => { const { playerScore, computerScore } = from.data; return states.PlayerChose(move, playerScore, computerScore); }, }, PlayerChose: { computerSelectMove: () => ({ from }) => { const { playerMove, playerScore, computerScore } = from.data; return states.Judging( playerMove, randomMove(), playerScore, computerScore ); }, }, Judging: { judge: () => ({ from }) => { const { playerMove, computerMove, playerScore, computerScore } = from.data; const winner = determineWinner(playerMove, computerMove); return states.RoundComplete( playerMove, computerMove, winner, playerScore + (winner === "player" ? 1 : 0), computerScore + (winner === "computer" ? 1 : 0) ); }, }, RoundComplete: { nextRound: () => ({ from }) => { const { playerScore, computerScore } = from.data; // Check if game over condition is met (e.g., score >= 5) if (playerScore >= 5 || computerScore >= 5) { const winner = playerScore >= 5 ? "player" : "computer"; return states.GameOver(winner, playerScore, computerScore); } return states.WaitingForPlayer(playerScore, computerScore); }, gameOver: "GameOver", }, GameOver: { newGame: "WaitingForPlayer", }, }, states.WaitingForPlayer(0, 0) ); return Object.assign(assignEventApi(machine), { randomMove, });}
import type { Move } from "./states";
export function determineWinner( playerMove: Move, computerMove: Move): "player" | "computer" | "tie" { if (playerMove === computerMove) return "tie"; if ( (playerMove === "rock" && computerMove === "scissors") || (playerMove === "paper" && computerMove === "rock") || (playerMove === "scissors" && computerMove === "paper") ) { return "player"; } return "computer";}export function randomMove() { const moves: Move[] = ["rock", "paper", "scissors"]; return moves[Math.floor(Math.random() * moves.length)];}
import { createRPSMachine } from "./machine";import type { Move } from "./states";
const getIcon = (move: Move | undefined) => { switch (move) { case "rock": return "✊"; case "paper": return "✋"; case "scissors": return "✌️"; default: return "❓"; }};
interface RPSAppViewProps { machine: ReturnType<typeof createRPSMachine>;}
export function RPSAppView({ machine }: RPSAppViewProps) { return ( <div className="p-4 border rounded"> <h2 className="text-xl font-bold text-center mb-3"> Rock Paper Scissors </h2>
{/* Score display */} <div className="flex justify-between text-lg font-medium mb-6 px-4"> <div>Player: {machine.getState().data.playerScore}</div> <div>Computer: {machine.getState().data.computerScore}</div> </div>
{/* Game content based on state */} {machine.getState().match({ WaitingForPlayer: () => ( <div className="text-center"> <h3 className="text-lg mb-3">Choose your move:</h3> <div className="flex justify-center gap-4"> <button className="move-btn p-4 bg-gray-100 hover:bg-gray-200 rounded-lg text-4xl" onClick={() => machine.selectMove("rock")} > ✊ </button> <button className="move-btn p-4 bg-gray-100 hover:bg-gray-200 rounded-lg text-4xl" onClick={() => machine.selectMove("paper")} > ✋ </button> <button className="move-btn p-4 bg-gray-100 hover:bg-gray-200 rounded-lg text-4xl" onClick={() => machine.selectMove("scissors")} > ✌️ </button> </div> </div> ),
PlayerChose: ({ playerMove }) => ( <div className="text-center"> <h3 className="text-lg mb-3">You chose {getIcon(playerMove)}</h3> <p className="mb-4">Computer is choosing...</p> <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onClick={() => machine.computerSelectMove()} > Continue </button> </div> ), Judging: ({ playerMove, computerMove }) => ( <div className="text-center"> <h3 className="text-lg mb-3">Judging...</h3> <p className="mb-4"> You chose {getIcon(playerMove)} vs Computer chose{" "} {getIcon(computerMove)} </p> <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onClick={() => machine.judge()} > See Result </button> </div> ), RoundComplete: (data: any) => ( <div className="text-center"> <h3 className="text-lg mb-3">Round Result:</h3> <div className="flex justify-around mb-4"> <div className="text-center"> <div className="text-4xl mb-2">{getIcon(data.playerMove)}</div> <div>You</div> </div> <div className="text-center"> <div className="text-4xl mb-2">vs</div> </div> <div className="text-center"> <div className="text-4xl mb-2"> {getIcon(data.computerMove)} </div> <div>Computer</div> </div> </div> <p className={`text-lg font-bold mb-4 ${ data.roundWinner === "player" ? "text-green-500" : data.roundWinner === "computer" ? "text-red-500" : "text-yellow-500" }`} > {data.roundWinner === "tie" ? "It's a tie!" : `${data.roundWinner === "player" ? "You" : "Computer"} won this round!`} </p> <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onClick={() => machine.nextRound()} > Next Round </button> </div> ),
GameOver: (data: any) => ( <div className="text-center"> <h3 className="text-xl font-bold mb-3">Game Over!</h3> <p className={`text-lg font-bold mb-4 ${ data.winner === "player" ? "text-green-500" : "text-red-500" }`} > {data.winner === "player" ? "You" : "Computer"} won the game! </p> <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onClick={() => machine.newGame()} > Play Again </button> </div> ), })} </div> );}
import { useState } from "react";import { RPSAppView } from "./RPSAppView";import { createRPSMachine } from "./machine";
export function RockPaperScissors() { const [game] = useState(() => createRPSMachine()); return <RPSAppView machine={game} />;}