starter repo

This commit is contained in:
wagslane
2024-04-02 12:38:06 -06:00
parent d4a1dc9bdf
commit 4885f7a113
18 changed files with 624 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.log

View File

@@ -1 +1,3 @@
# learn-pub-sub-starter
# learn-pub-sub-starter (Peril)
This is the starter code used in Boot.dev's [Learn Pub/Sub](https://learn.boot.dev/learn-pub-sub) course.

7
cmd/client/main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("Starting Peril client...")
}

7
cmd/server/main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("Starting Peril server...")
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/bootdotdev/learn-pub-sub-starter
go 1.22.1

0
go.sum Normal file
View File

View File

@@ -0,0 +1,52 @@
package gamelogic
type Player struct {
Username string
Units map[int]Unit
}
type UnitRank string
const (
RankInfantry = "infantry"
RankCavalry = "cavalry"
RankArtillery = "artillery"
)
type Unit struct {
ID int
Rank UnitRank
Location Location
}
type ArmyMove struct {
Player Player
Units []Unit
ToLocation Location
}
type RecognitionOfWar struct {
Attacker Player
Defender Player
}
type Location string
func getAllRanks() map[UnitRank]struct{} {
return map[UnitRank]struct{}{
RankInfantry: {},
RankCavalry: {},
RankArtillery: {},
}
}
func getAllLocations() map[Location]struct{} {
return map[Location]struct{}{
"americas": {},
"europe": {},
"africa": {},
"asia": {},
"australia": {},
"antarctica": {},
}
}

View File

@@ -0,0 +1,92 @@
package gamelogic
import (
"bufio"
"errors"
"fmt"
"math/rand"
"os"
"strings"
)
func PrintClientHelp() {
fmt.Println("Possible commands:")
fmt.Println("* move <location> <unitID> <unitID> <unitID>...")
fmt.Println(" example:")
fmt.Println(" move asia 1")
fmt.Println("* spawn <location> <rank>")
fmt.Println(" example:")
fmt.Println(" spawn europe infantry")
fmt.Println("* status")
fmt.Println("* spam <n>")
fmt.Println(" example:")
fmt.Println(" spam 5")
fmt.Println("* quit")
fmt.Println("* help")
}
func ClientWelcome() (string, error) {
fmt.Println("Welcome to the Peril client!")
fmt.Println("Please enter your username:")
words := GetInput()
if len(words) == 0 {
return "", errors.New("you must enter a username. goodbye")
}
username := words[0]
fmt.Printf("Welcome, %s!\n", username)
PrintClientHelp()
return username, nil
}
func PrintServerHelp() {
fmt.Println("Possible commands:")
fmt.Println("* pause")
fmt.Println("* resume")
fmt.Println("* quit")
fmt.Println("* help")
}
func GetInput() []string {
fmt.Print("> ")
scanner := bufio.NewScanner(os.Stdin)
scanned := scanner.Scan()
if !scanned {
return nil
}
line := scanner.Text()
line = strings.TrimSpace(line)
return strings.Fields(line)
}
func GetMaliciousLog() string {
possibleLogs := []string{
"Never interrupt your enemy when he is making a mistake.",
"The hardest thing of all for a soldier is to retreat.",
"A soldier will fight long and hard for a bit of colored ribbon.",
"It is well that war is so terrible, otherwise we should grow too fond of it.",
"The art of war is simple enough. Find out where your enemy is. Get at him as soon as you can. Strike him as hard as you can, and keep moving on.",
"All warfare is based on deception.",
}
randomIndex := rand.Intn(len(possibleLogs))
msg := possibleLogs[randomIndex]
return msg
}
func PrintQuit() {
fmt.Println("I hate this game! (╯°□°)╯︵ ┻━┻")
}
func (gs *GameState) CommandStatus() {
if gs.isPaused() {
fmt.Println("The game is paused.")
return
} else {
fmt.Println("The game is not paused.")
}
p := gs.GetPlayerSnap()
fmt.Printf("You are %s, and you have %d units.\n", p.Username, len(p.Units))
for _, unit := range p.Units {
fmt.Printf("* %v: %v, %v\n", unit.ID, unit.Location, unit.Rank)
}
}

View File

@@ -0,0 +1,96 @@
package gamelogic
import (
"sync"
)
type GameState struct {
Player Player
Paused bool
mu *sync.RWMutex
}
func NewGameState(username string) *GameState {
return &GameState{
Player: Player{
Username: username,
Units: map[int]Unit{},
},
Paused: false,
mu: &sync.RWMutex{},
}
}
func (gs *GameState) resumeGame() {
gs.mu.Lock()
defer gs.mu.Unlock()
gs.Paused = false
}
func (gs *GameState) pauseGame() {
gs.mu.Lock()
defer gs.mu.Unlock()
gs.Paused = true
}
func (gs *GameState) isPaused() bool {
gs.mu.RLock()
defer gs.mu.RUnlock()
return gs.Paused
}
func (gs *GameState) addUnit(u Unit) {
gs.mu.Lock()
defer gs.mu.Unlock()
gs.Player.Units[u.ID] = u
}
func (gs *GameState) removeUnitsInLocation(loc Location) {
gs.mu.Lock()
defer gs.mu.Unlock()
for k, v := range gs.Player.Units {
if v.Location == loc {
delete(gs.Player.Units, k)
}
}
}
func (gs *GameState) UpdateUnit(u Unit) {
gs.mu.Lock()
defer gs.mu.Unlock()
gs.Player.Units[u.ID] = u
}
func (gs *GameState) GetUsername() string {
return gs.Player.Username
}
func (gs *GameState) getUnitsSnap() []Unit {
gs.mu.RLock()
defer gs.mu.RUnlock()
Units := []Unit{}
for _, v := range gs.Player.Units {
Units = append(Units, v)
}
return Units
}
func (gs *GameState) GetUnit(id int) (Unit, bool) {
gs.mu.RLock()
defer gs.mu.RUnlock()
u, ok := gs.Player.Units[id]
return u, ok
}
func (gs *GameState) GetPlayerSnap() Player {
gs.mu.RLock()
defer gs.mu.RUnlock()
Units := map[int]Unit{}
for k, v := range gs.Player.Units {
Units[k] = v
}
return Player{
Username: gs.Player.Username,
Units: Units,
}
}

View File

@@ -0,0 +1,32 @@
package gamelogic
import (
"fmt"
"log"
"os"
"time"
"github.com/bootdotdev/learn-pub-sub-starter/internal/routing"
)
const logsFile = "game.log"
const writeToDiskSleep = 1 * time.Second
func WriteLog(gamelog routing.GameLog) error {
log.Printf("received game log...")
time.Sleep(writeToDiskSleep)
f, err := os.OpenFile(logsFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("could not open logs file: %v", err)
}
defer f.Close()
str := fmt.Sprintf("%v %v: %v\n", gamelog.CurrentTime.Format(time.RFC3339), gamelog.Username, gamelog.Message)
_, err = f.WriteString(str)
if err != nil {
return fmt.Errorf("could not write to logs file: %v", err)
}
return nil
}

View File

@@ -0,0 +1,92 @@
package gamelogic
import (
"errors"
"fmt"
"strconv"
)
type MoveOutcome int
const (
MoveOutcomeSamePlayer MoveOutcome = iota
MoveOutComeSafe
MoveOutcomeMakeWar
)
func (gs *GameState) HandleMove(move ArmyMove) MoveOutcome {
defer fmt.Println("------------------------")
player := gs.GetPlayerSnap()
fmt.Println()
fmt.Println("==== Move Detected ====")
fmt.Printf("%s is moving %v unit(s) to %s\n", move.Player.Username, len(move.Units), move.ToLocation)
for _, unit := range move.Units {
fmt.Printf("* %v\n", unit.Rank)
}
if player.Username == move.Player.Username {
return MoveOutcomeSamePlayer
}
overlappingLocation := getOverlappingLocation(player, move.Player)
if overlappingLocation != "" {
fmt.Printf("You have units in %s! You are at war with %s!\n", overlappingLocation, move.Player.Username)
return MoveOutcomeMakeWar
}
fmt.Printf("You are safe from %s's units.\n", move.Player.Username)
return MoveOutComeSafe
}
func getOverlappingLocation(p1 Player, p2 Player) Location {
for _, u1 := range p1.Units {
for _, u2 := range p2.Units {
if u1.Location == u2.Location {
return u1.Location
}
}
}
return ""
}
func (gs *GameState) CommandMove(words []string) (ArmyMove, error) {
if gs.isPaused() {
return ArmyMove{}, errors.New("the game is paused, you can not move units")
}
if len(words) < 3 {
return ArmyMove{}, errors.New("usage: move <location> <unitID> <unitID> <unitID> etc")
}
newLocation := Location(words[1])
locations := getAllLocations()
if _, ok := locations[newLocation]; !ok {
return ArmyMove{}, fmt.Errorf("error: %s is not a valid location", newLocation)
}
unitIDs := []int{}
for _, word := range words[2:] {
id := word
unitID, err := strconv.Atoi(id)
if err != nil {
return ArmyMove{}, fmt.Errorf("error: %s is not a valid unit ID", id)
}
unitIDs = append(unitIDs, unitID)
}
newUnits := []Unit{}
for _, unitID := range unitIDs {
unit, ok := gs.GetUnit(unitID)
if !ok {
return ArmyMove{}, fmt.Errorf("error: unit with ID %v not found", unitID)
}
unit.Location = newLocation
gs.UpdateUnit(unit)
newUnits = append(newUnits, unit)
}
mv := ArmyMove{
ToLocation: newLocation,
Units: newUnits,
Player: gs.GetPlayerSnap(),
}
fmt.Printf("Moved %v units to %s\n", len(mv.Units), mv.ToLocation)
return mv, nil
}

View File

@@ -0,0 +1,19 @@
package gamelogic
import (
"fmt"
"github.com/bootdotdev/learn-pub-sub-starter/internal/routing"
)
func (gs *GameState) HandlePause(ps routing.PlayingState) {
defer fmt.Println("------------------------")
fmt.Println()
if ps.IsPaused {
fmt.Println("==== Pause Detected ====")
gs.pauseGame()
} else {
fmt.Println("==== Resume Detected ====")
gs.resumeGame()
}
}

View File

@@ -0,0 +1,34 @@
package gamelogic
import (
"errors"
"fmt"
)
func (gs *GameState) CommandSpawn(words []string) error {
if len(words) < 3 {
return errors.New("usage: spawn <location> <rank>")
}
locationName := words[1]
locations := getAllLocations()
if _, ok := locations[Location(locationName)]; !ok {
return fmt.Errorf("error: %s is not a valid location", locationName)
}
rank := words[2]
units := getAllRanks()
if _, ok := units[UnitRank(rank)]; !ok {
return fmt.Errorf("error: %s is not a valid unit", rank)
}
id := len(gs.getUnitsSnap()) + 1
gs.addUnit(Unit{
ID: id,
Rank: UnitRank(rank),
Location: Location(locationName),
})
fmt.Printf("Spawned a(n) %s in %s with id %v\n", rank, locationName, id)
return nil
}

105
internal/gamelogic/war.go Normal file
View File

@@ -0,0 +1,105 @@
package gamelogic
import (
"fmt"
)
type WarOutcome int
const (
WarOutcomeNotInvolved WarOutcome = iota
WarOutcomeNoUnits
WarOutcomeYouWon
WarOutcomeOpponentWon
WarOutcomeDraw
)
func (gs *GameState) HandleWar(rw RecognitionOfWar) (outcome WarOutcome, winner string, loser string) {
defer fmt.Println("------------------------")
fmt.Println()
fmt.Println("==== War Declared ====")
fmt.Printf("%s has declared war on %s!\n", rw.Attacker.Username, rw.Defender.Username)
player := gs.GetPlayerSnap()
if player.Username == rw.Defender.Username {
fmt.Printf("%s, you published the war.\n", player.Username)
return WarOutcomeNotInvolved, "", ""
}
if player.Username != rw.Attacker.Username {
fmt.Printf("%s, you are not involved in this war.\n", player.Username)
return WarOutcomeNotInvolved, "", ""
}
overlappingLocation := getOverlappingLocation(rw.Attacker, rw.Defender)
if overlappingLocation == "" {
fmt.Printf("Error! No units are in the same location. No war will be fought.\n")
return WarOutcomeNoUnits, "", ""
}
attackerUnits := []Unit{}
defenderUnits := []Unit{}
for _, unit := range rw.Attacker.Units {
if unit.Location == overlappingLocation {
attackerUnits = append(attackerUnits, unit)
}
}
for _, unit := range rw.Defender.Units {
if unit.Location == overlappingLocation {
defenderUnits = append(defenderUnits, unit)
}
}
fmt.Printf("%s's units:\n", rw.Attacker.Username)
for _, unit := range attackerUnits {
fmt.Printf(" * %v\n", unit.Rank)
}
fmt.Printf("%s's units:\n", rw.Defender.Username)
for _, unit := range defenderUnits {
fmt.Printf(" * %v\n", unit.Rank)
}
attackerPower := unitsToPowerLevel(attackerUnits)
defenderPower := unitsToPowerLevel(defenderUnits)
fmt.Printf("Attacker has a power level of %v\n", attackerPower)
fmt.Printf("Defender has a power level of %v\n", defenderPower)
if attackerPower > defenderPower {
fmt.Printf("%s has won the war!\n", rw.Attacker.Username)
if player.Username == rw.Defender.Username {
fmt.Println("You have lost the war!")
gs.removeUnitsInLocation(overlappingLocation)
fmt.Printf("Your units in %s have been killed.\n", overlappingLocation)
return WarOutcomeOpponentWon, rw.Attacker.Username, rw.Defender.Username
}
return WarOutcomeYouWon, rw.Attacker.Username, rw.Defender.Username
} else if defenderPower > attackerPower {
fmt.Printf("%s has won the war!\n", rw.Defender.Username)
if player.Username == rw.Attacker.Username {
fmt.Println("You have lost the war!")
gs.removeUnitsInLocation(overlappingLocation)
fmt.Printf("Your units in %s have been killed.\n", overlappingLocation)
return WarOutcomeOpponentWon, rw.Defender.Username, rw.Attacker.Username
}
return WarOutcomeYouWon, rw.Defender.Username, rw.Attacker.Username
}
fmt.Println("The war ended in a draw!")
fmt.Printf("Your units in %s have been killed.\n", overlappingLocation)
gs.removeUnitsInLocation(overlappingLocation)
return WarOutcomeDraw, rw.Attacker.Username, rw.Defender.Username
}
func unitsToPowerLevel(units []Unit) int {
power := 0
for _, unit := range units {
if unit.Rank == RankArtillery {
power += 10
}
if unit.Rank == RankCavalry {
power += 5
}
if unit.Rank == RankInfantry {
power += 1
}
}
return power
}

View File

@@ -0,0 +1,13 @@
package routing
import "time"
type PlayingState struct {
IsPaused bool
}
type GameLog struct {
CurrentTime time.Time
Message string
Username string
}

View File

@@ -0,0 +1,16 @@
package routing
const (
ArmyMovesPrefix = "army_moves"
WarRecognitionsPrefix = "war"
PauseKey = "pause"
GameLogSlug = "game_logs"
)
const (
ExchangePerilDirect = "peril_direct"
ExchangePerilTopic = "peril_topic"
)

33
multiserver.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Check if the number of instances was provided
if [ -z "$1" ]; then
echo "Usage: $0 <number-of-instances>"
exit 1
fi
num_instances=$1
# Array to store process IDs
declare -a pids
# Function to kill all processes when Ctrl+C is pressed
cleanup() {
echo "Terminating all instances of ./cmd/server..."
for pid in "${pids[@]}"; do
kill -SIGTERM "$pid"
done
exit
}
# Setup trap for SIGINT
trap 'cleanup' SIGINT
# Start the specified number of instances of the program in the background
for (( i=0; i<num_instances; i++ )); do
go run ./cmd/server &
pids+=($!)
done
# Wait for all background processes to finish
wait

19
rabbit.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
case "$1" in
start)
echo "Starting RabbitMQ container..."
docker run -d --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.13-management
;;
stop)
echo "Stopping RabbitMQ container..."
docker stop rabbitmq
;;
logs)
echo "Fetching logs for RabbitMQ container..."
docker logs -f rabbitmq
;;
*)
echo "Usage: $0 {start|stop|logs}"
exit 1
esac