Skip to content

Commit 6885609

Browse files
Merge pull request #8 from chrisreddington/rockpaperscissors
Add Rock Paper Scissors game with CLI and unit tests
2 parents 4cdd2e8 + 497e899 commit 6885609

5 files changed

Lines changed: 664 additions & 0 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ gh game cointoss heads # or tails
2020

2121
The game will continue as long as you keep guessing correctly, allowing you to build up a streak. You can quit at any time by selecting "Quit" when prompted for your next guess.
2222

23+
### Rock Paper Scissors
24+
25+
Play Rock Paper Scissors against the computer. Best of 3, 5, 7, or 9 rounds.
26+
27+
```sh
28+
gh game rockpaperscissors
29+
```
30+
31+
Choose your move (rock, paper, or scissors) in each round, and the computer will randomly select its move. The game follows standard Rock Paper Scissors rules:
32+
- Rock crushes Scissors
33+
- Scissors cuts Paper
34+
- Paper covers Rock
35+
2336
### Tic Tac Toe
2437

2538
Play the classic game of Tic Tac Toe against another player. Players take turns placing X's and O's on a 3x3 grid, trying to get three in a row horizontally, vertically, or diagonally.

cmd/rockpaperscissors.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// This file is an entrypoint for the Rock Paper Scissors game. It uses the cobra cmd to execute the game.
2+
// The game logic is in the internal/rockpaperscissors package.
3+
4+
// This file sets up the command line interface for the game. It should call the PlayGame function from the rockpaperscissors package.
5+
6+
package cmd
7+
8+
import (
9+
"os"
10+
11+
"github.com/chrisreddington/gh-game/internal/rockpaperscissors"
12+
userPrompt "github.com/cli/go-gh/v2/pkg/prompter"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
// rootCmd represents the base command when called without any subcommands
17+
var rockPaperScissorsCmd = &cobra.Command{
18+
Use: "rockpaperscissors",
19+
Short: "A simple Rock Paper Scissors game",
20+
Long: `A simple Rock Paper Scissors game that allows you to play against the computer.
21+
You can choose from rock, paper, or scissors. The computer will randomly choose its move and the winner will be determined based on the rules of the game.`,
22+
Run: func(cmd *cobra.Command, args []string) {
23+
input := userPrompt.New(os.Stdin, os.Stdout, os.Stderr)
24+
rockpaperscissors.PlayGame(input)
25+
},
26+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ func init() {
1818
rootCmd.AddCommand(whoamiCmd)
1919
rootCmd.AddCommand(cointossCmd)
2020
rootCmd.AddCommand(tictactoeCmd)
21+
rootCmd.AddCommand(rockPaperScissorsCmd)
2122
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package rockpaperscissors
2+
3+
import (
4+
"fmt"
5+
"math/rand"
6+
"strings"
7+
"time"
8+
)
9+
10+
// Game options
11+
var (
12+
options = []string{"rock", "paper", "scissors", "exit"}
13+
)
14+
15+
// Game represents a single game of Rock Paper Scissors.
16+
type Game struct {
17+
// PlayerChoice is the choice made by the player.
18+
PlayerChoice string
19+
// ComputerChoice is the choice made by the computer.
20+
ComputerChoice string
21+
// Winner is the winner of the current round.
22+
Winner string
23+
// PlayerScore tracks the player's wins
24+
PlayerScore int
25+
// ComputerScore tracks the computer's wins
26+
ComputerScore int
27+
// BestOf determines how many games to play (e.g., best of 3, 5, 7)
28+
BestOf int
29+
// GameOver is a flag to indicate if the game is over.
30+
GameOver bool
31+
// GameOverMessage is the message to display when the game is over.
32+
GameOverMessage string
33+
// GamesPlayed tracks the number of games played
34+
GamesPlayed int
35+
}
36+
37+
// Prompter defines an interface for getting user input
38+
type Prompter interface {
39+
Select(prompt, defaultValue string, options []string) (int, error)
40+
}
41+
42+
func NewGame(bestOf int) *Game {
43+
if bestOf%2 == 0 {
44+
bestOf++ // Ensure we have an odd number for "best of"
45+
}
46+
return &Game{
47+
PlayerChoice: "",
48+
ComputerChoice: "",
49+
Winner: "",
50+
GamesPlayed: 0,
51+
PlayerScore: 0,
52+
ComputerScore: 0,
53+
BestOf: bestOf,
54+
GameOver: false,
55+
GameOverMessage: "",
56+
}
57+
}
58+
59+
// Play plays a single round of Rock Paper Scissors.
60+
func (g *Game) Play(playerChoice string) {
61+
if playerChoice == "exit" {
62+
g.GameOver = true
63+
g.GameOverMessage = "Game ended by player"
64+
return
65+
}
66+
67+
g.PlayerChoice = playerChoice
68+
g.ComputerChoice = g.getComputerChoice()
69+
g.Winner = g.getWinner()
70+
g.updateScore()
71+
g.GamesPlayed++
72+
g.GameOver = g.isGameOver()
73+
if g.GameOver {
74+
g.GameOverMessage = g.getGameOverMessage()
75+
}
76+
}
77+
78+
// getComputerChoice returns the choice made by the computer.
79+
func (g *Game) getComputerChoice() string {
80+
rand.Seed(time.Now().UnixNano())
81+
// Only use the game options excluding "exit"
82+
choices := options[:len(options)-1]
83+
return choices[rand.Intn(len(choices))]
84+
}
85+
86+
// getWinner returns the winner of the current round.
87+
func (g *Game) getWinner() string {
88+
if g.PlayerChoice == g.ComputerChoice {
89+
return "draw"
90+
}
91+
if (g.PlayerChoice == "rock" && g.ComputerChoice == "scissors") ||
92+
(g.PlayerChoice == "paper" && g.ComputerChoice == "rock") ||
93+
(g.PlayerChoice == "scissors" && g.ComputerChoice == "paper") {
94+
return "player"
95+
}
96+
return "computer"
97+
}
98+
99+
// updateScore updates the score based on the round winner
100+
func (g *Game) updateScore() {
101+
if g.Winner == "player" {
102+
g.PlayerScore++
103+
} else if g.Winner == "computer" {
104+
g.ComputerScore++
105+
}
106+
}
107+
108+
// isGameOver returns true if the game is over.
109+
func (g *Game) isGameOver() bool {
110+
winsNeeded := (g.BestOf / 2) + 1
111+
112+
// Game is over if:
113+
// 1. Either player has reached the required wins, or
114+
// 2. We've played all games in the series
115+
return g.PlayerScore >= winsNeeded ||
116+
g.ComputerScore >= winsNeeded ||
117+
g.GamesPlayed >= g.BestOf
118+
}
119+
120+
// getGameOverMessage returns the message to display when the game is over.
121+
func (g *Game) getGameOverMessage() string {
122+
if g.PlayerScore > g.ComputerScore {
123+
return fmt.Sprintf("GAME OVER: Player WINS (%d - %d)", g.PlayerScore, g.ComputerScore)
124+
} else if g.ComputerScore > g.PlayerScore {
125+
return fmt.Sprintf("GAME OVER: Player LOSES (%d - %d)", g.PlayerScore, g.ComputerScore)
126+
}
127+
return fmt.Sprintf("GAME OVER: DRAW (%d - %d)", g.PlayerScore, g.ComputerScore)
128+
}
129+
130+
// getRoundResultMessage returns a concise message about the round result
131+
func (g *Game) getRoundResultMessage() string {
132+
// Capitalize the first letter of choices for better display
133+
playerChoice := strings.Title(g.PlayerChoice)
134+
computerChoice := strings.Title(g.ComputerChoice)
135+
136+
if g.Winner == "draw" {
137+
return fmt.Sprintf("Draw! Player (%s) - CPU (%s)", playerChoice, computerChoice)
138+
} else if g.Winner == "player" {
139+
return fmt.Sprintf("Player (%s) beats CPU (%s)", playerChoice, computerChoice)
140+
} else {
141+
return fmt.Sprintf("Player (%s) loses to CPU (%s)", playerChoice, computerChoice)
142+
}
143+
}
144+
145+
// PlayGame plays a game of Rock Paper Scissors.
146+
func PlayGame(prompter Prompter) {
147+
// Get the number of rounds from the user
148+
roundOptions := []string{"3", "5", "7", "9"}
149+
roundIndex, err := prompter.Select("How many rounds would you like to play (best of)?", "3", roundOptions)
150+
if err != nil {
151+
fmt.Printf("Error getting number of rounds: %v\n", err)
152+
return
153+
}
154+
bestOf := 3 // Default value
155+
if roundIndex >= 0 && roundIndex < len(roundOptions) {
156+
bestOf = parseInt(roundOptions[roundIndex])
157+
}
158+
159+
game := NewGame(bestOf)
160+
fmt.Printf("Playing best of %d games\n", bestOf)
161+
162+
for !game.GameOver {
163+
fmt.Printf("\nCurrent score - Player: %d, Computer: %d\n", game.PlayerScore, game.ComputerScore)
164+
165+
// Get player choice using prompter
166+
playerChoiceIndex, err := prompter.Select("Choose your move", "rock", options)
167+
if err != nil {
168+
fmt.Printf("Error getting player choice: %v\n", err)
169+
return
170+
}
171+
playerChoice := options[playerChoiceIndex]
172+
173+
game.Play(playerChoice)
174+
if playerChoice == "exit" {
175+
break
176+
}
177+
178+
// Display a more concise round result
179+
fmt.Println(game.getRoundResultMessage())
180+
}
181+
fmt.Println(game.GameOverMessage)
182+
}
183+
184+
// parseInt safely converts a string to an integer
185+
func parseInt(s string) int {
186+
val := 3 // Default value
187+
fmt.Sscanf(s, "%d", &val)
188+
if val <= 0 {
189+
return 3
190+
}
191+
if val%2 == 0 {
192+
val++ // Convert even numbers to next odd number
193+
}
194+
return val
195+
}

0 commit comments

Comments
 (0)