From 0045790ec448310ac3b9489095a3115c5fd04db9 Mon Sep 17 00:00:00 2001 From: gabe farrell Date: Sun, 20 Feb 2022 15:47:34 -0500 Subject: [PATCH] Initial Commit --- README.md | Bin 0 -> 36 bytes board/board.go | 304 +++++++++++++++++++++++++++++++++++++++++++ board/clearscreen.go | 33 +++++ cprint/cprint.go | 58 +++++++++ dice.go | 122 +++++++++++++++++ go.mod | 9 ++ go.sum | 7 + main.go | 115 ++++++++++++++++ player/player.go | 299 ++++++++++++++++++++++++++++++++++++++++++ tests/board_test.go | 66 ++++++++++ 10 files changed, 1013 insertions(+) create mode 100644 README.md create mode 100644 board/board.go create mode 100644 board/clearscreen.go create mode 100644 cprint/cprint.go create mode 100644 dice.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 player/player.go create mode 100644 tests/board_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9b57276b2d90ad61dcba4d57add212104cc32797 GIT binary patch literal 36 pcmezWPnki1A)O(gL6;$kA(5d32y+-x8HyP)7%~|O7 10 || row < 'a' || row > 'j' { + return Point{}, fmt.Errorf("Invalid coordinate") + } else { + return Point{row, col}, nil + } +} + +func (p Point) IsValid() bool { + if p.Row > 'j' || p.Row < 'a' || p.Col > 10 || p.Col < 1 { + return false + } + return true +} + +func (p Point) NudgeRight(i int) Point { + return Point{p.Row, p.Col + i} +} +func (p Point) NudgeUp(i int) Point { + return Point{p.Row - rune(i), p.Col} +} +func (p Point) NudgeDown(i int) Point { + return Point{p.Row + rune(i), p.Col} +} +func (p Point) NudgeLeft(i int) Point { + return Point{p.Row, p.Col - i} +} + +func CoordToPoint(b rune, i int) Point { + if unicode.IsUpper(b) { + b = unicode.ToLower(b) + } + return Point{b, i} +} + +func (b Board) print() { + fmt.Println(" 1 2 3 4 5 6 7 8 9 10") + for c := 'a'; c <= 'j'; c++ { + fmt.Printf("%c", c) + for i := 1; i <= 10; i++ { + cprint.Printf["White"](" " + string(b.Board[CoordToPoint(c, i)])) + } + fmt.Println() + } +} + +func (b Board) GameOver() bool { + for c := 'a'; c <= 'j'; c++ { + for i := 1; i <= 10; i++ { + if b.Board[CoordToPoint(c, i)] == 'O' { + return false + } + } + } + return true +} + +func (b Board) shipExistsAt(coords []Point) bool { + for _, p := range coords { + if b.Board[p] != '-' { + return true + } + } + return false +} + +// ok so for some stupid reason that i cant figure out, even if i pass the ship +// parameter here by reference the new coordinate array that the ship contains +// WILL NOT be updated when the function returns, and i literally have no idea why +// so thats why it returns the Ship with the new array as well as the error +func (b *Board) PlaceShip(s Ship, p Point, vertical bool) (Ship, error) { + switch vertical { + case true: + if p.Row+rune(s.Length) > 'j' { + break + } + for i := 0; i < s.Length; i++ { + s.Coords = append(s.Coords, Point{p.Row + rune(i), p.Col}) + } + if !b.shipExistsAt(s.Coords) { + for _, c := range s.Coords { + b.Board[c] = 'O' + } + } else { + s.Coords = make([]Point, 0) + return s, fmt.Errorf("Ship already exists at that point") + } + return s, nil + case false: + if p.Col+s.Length > 10 { + break + } + for i := 0; i < s.Length; i++ { + s.Coords = append(s.Coords, Point{p.Row, p.Col + i}) + } + if !b.shipExistsAt(s.Coords) { + for _, c := range s.Coords { + b.Board[c] = 'O' + } + } else { + s.Coords = make([]Point, 0) + return s, fmt.Errorf("Ship already exists at that point") + } + return s, nil + default: + + s.Coords = make([]Point, 0) + return s, fmt.Errorf("Coordinate out of bounds") + } + + s.Coords = make([]Point, 0) + return s, fmt.Errorf("Coordinate out of bounds.") +} + +func (b *Board) PlaceShips() { + stdin := bufio.NewReader(os.Stdin) + var vert rune + var vertBool bool + for i := range b.Ships { + s := b.Ships[i] + for { + b.print() + cprint.Printf["Yellow"]("Where do you want your %s? (Length %d): ", s.Name, s.Length) + p, err := InputPoint() + if err != nil { + cprint.Printf["Red"]("Error: " + err.Error() + "\n") + } + cprint.Printf["White"]("Placed Horizontally or Vertically (h or v): ") + _, err = fmt.Scanf("%c", &vert) + stdin.ReadString('\n') + if err != nil { + stdin.ReadString('\n') + cprint.Printf["Red"]("Bad input! Enter 'v', 'h', 'vertical', or 'horizontal'\n") + } + vertBool = HVToBool(vert) + if b.Ships[i], err = b.PlaceShip(s, p, vertBool); err == nil { + s := b.Ships[i] + ClearScreen() + if vertBool { + cprint.Printf["Cyan"]("%s placed vertically at coordinate [%c%d]!\n", s.Name, p.Row, p.Col) + } else { + cprint.Printf["Cyan"]("%s placed horizontally at coordinate [%c%d]!\n", s.Name, p.Row, p.Col) + } + + break + } else { + cprint.Printf["Red"]("Error: " + err.Error() + "\n") + } + } + } +} + +func (b *Board) AutoPlaceShips() { + var err error + for i := range b.Ships { + for { + s := b.Ships[i] + p := RandPoint() + v := rand.Intn(2) + b.Ships[i], err = b.PlaceShip(s, p, itob(v)) + s = b.Ships[i] + if err == nil { + break + } + } + } +} + +func (b Board) Sunk(s Ship) bool { + for _, p := range s.Coords { + if b.Board[p] == 'O' { //there are still spots yet to be hit on the ship + time.Sleep(5) + return false + } + } + return true +} + +/* + returns whether or not the shot hit, and if the ship has been sunk, + it returns the sunken ship. otherwise, it returns an empty Ship +*/ +func (b Board) CheckForHit(c Point) (hit bool, ship Ship) { + if b.Board[c] == 'O' { + hit = true + for _, s := range b.Ships { + for _, p := range s.Coords { + if p == c { + ship = s + break + } + } + if ship.Name == s.Name { + break + } + } + } else { + hit = false + ship = Ship{} + } + return hit, ship +} diff --git a/board/clearscreen.go b/board/clearscreen.go new file mode 100644 index 0000000..8471e8b --- /dev/null +++ b/board/clearscreen.go @@ -0,0 +1,33 @@ +package board + +import ( + "fmt" + + "github.com/ghfarrell/go-battleship/cprint" +) + +// clearscreen and printlogo functions have to be in this package so that I can use +// them in the ship placement loop :/ +func PrintLogo() { + cprint.Printf["Magenta"](" /$$$$$$$ /$$$$$$ /$$$$$$$$/$$$$$$$$/$$ /$$$$$$$$ \n") + cprint.Printf["Magenta"]("| $$__ $$ /$$__ $$|__ $$__/__ $$__/ $$ | $$_____/ \n") + cprint.Printf["Magenta"]("| $$ \\ $$| $$ \\ $$ | $$ | $$ | $$ | $$ \n") + cprint.Printf["Magenta"]("| $$$$$$$ | $$$$$$$$ | $$ | $$ | $$ | $$$$$ \n") + cprint.Printf["Magenta"]("| $$__ $$| $$__ $$ | $$ | $$ | $$ | $$__/ \n") + cprint.Printf["Magenta"]("| $$ \\ $$| $$ | $$ | $$ | $$ | $$ | $$ \n") + cprint.Printf["Magenta"]("| $$$$$$$/| $$ | $$ | $$ | $$ | $$$$$$$$| $$$$$$$$| \n") + cprint.Printf["Magenta"]("|_______/ |__/ |__/ |__/ |__/ |________/|________/ \n") + cprint.Printf["Magenta"](" /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$\n") + cprint.Printf["Magenta"](" /$$__ $$| $$ | $$|_ $$_/| $$__ $$\n") + cprint.Printf["Magenta"]("| $$ \\__/| $$ | $$ | $$ | $$ \\ $$\n") + cprint.Printf["Magenta"]("| $$$$$$ | $$$$$$$$ | $$ | $$$$$$$/\n") + cprint.Printf["Magenta"](" \\____ $$| $$__ $$ | $$ | $$____/ \n") + cprint.Printf["Magenta"](" /$$ \\ $$| $$ | $$ | $$ | $$ \n") + cprint.Printf["Magenta"](" $$$$$$/| $$ | $$ /$$$$$$| $$ \n") + cprint.Printf["Magenta"](" \\______/ |__/ |__/|______/|__/ \n") + cprint.Printf["Red"]("Press ctrl + c at any time to quit.\n\n") +} +func ClearScreen() { + fmt.Print("\033[H\033[2J") + PrintLogo() +} diff --git a/cprint/cprint.go b/cprint/cprint.go new file mode 100644 index 0000000..1d0c98f --- /dev/null +++ b/cprint/cprint.go @@ -0,0 +1,58 @@ +package cprint + +import ( + "fmt" + + "github.com/jwalton/go-supportscolor" +) + +var Printf map[string]func(string, ...interface{}) + +func init() { + colors := []string{ + "Red", + "Blue", + "Yellow", + "White", + "Magenta", + "Cyan", + "Green", + } + Printf = make(map[string]func(s string, a ...interface{})) + if supportscolor.Stdout().SupportsColor { + Printf["Yellow"] = func(s string, a ...interface{}) { + p := "\u001b[33;1m" + s + "\u001b[0m" + fmt.Printf(p, a...) + } + Printf["Red"] = func(s string, a ...interface{}) { + p := "\u001b[31;1m" + s + "\u001b[0m" + fmt.Printf(p, a...) + } + Printf["Cyan"] = func(s string, a ...interface{}) { + p := "\u001b[36;1m" + s + "\u001b[0m" + fmt.Printf(p, a...) + } + Printf["Magenta"] = func(s string, a ...interface{}) { + p := "\u001b[35;1m" + s + "\u001b[0m" + fmt.Printf(p, a...) + } + Printf["White"] = func(s string, a ...interface{}) { + p := "\u001b[37;1m" + s + "\u001b[0m" + fmt.Printf(p, a...) + } + Printf["Green"] = func(s string, a ...interface{}) { + p := "\u001b[32;1m" + s + "\u001b[0m" + fmt.Printf(p, a...) + } + Printf["Blue"] = func(s string, a ...interface{}) { + p := "\u001b[34;1m" + s + "\u001b[0m" + fmt.Printf(p, a...) + } + } else { + for _, c := range colors { + Printf[c] = func(s string, a ...interface{}) { + fmt.Printf(s, a...) + } + } + } +} diff --git a/dice.go b/dice.go new file mode 100644 index 0000000..71ffdff --- /dev/null +++ b/dice.go @@ -0,0 +1,122 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/ghfarrell/go-battleship/board" + "github.com/ghfarrell/go-battleship/cprint" +) + +/* + + ____________ + | | + | | + | | + | | + |____________| + +*/ +func RollTheBones() int { + for i := 0; i < 2; i++ { + board.ClearScreen() + cprint.Printf["White"](" _______\n") + cprint.Printf["White"](" /\\ \\\n") + cprint.Printf["White"](" /()\\ () \\\n") + cprint.Printf["White"]("/ \\_______\\\n") + cprint.Printf["White"]("\\ /() /\n") + cprint.Printf["White"](" \\()/ () /\n") + cprint.Printf["White"](" \\/_____()/\n") + time.Sleep(250 * time.Millisecond) + board.ClearScreen() + cprint.Printf["White"](" _______\n") + cprint.Printf["White"](" /\\ () ()\\\n") + cprint.Printf["White"](" /()\\ () \\\n") + cprint.Printf["White"]("() \\()___()\\\n") + cprint.Printf["White"]("\\ ()/ /\n") + cprint.Printf["White"](" \\()/ () /\n") + cprint.Printf["White"](" \\/_______/\n") + time.Sleep(250 * time.Millisecond) + board.ClearScreen() + cprint.Printf["White"](" _______\n") + cprint.Printf["White"](" /\\() ()\\\n") + cprint.Printf["White"](" /()\\() ()\\\n") + cprint.Printf["White"]("/() \\()___()\\\n") + cprint.Printf["White"]("\\ ()/() /\n") + cprint.Printf["White"](" \\()/ /\n") + cprint.Printf["White"](" \\/_____()/\n") + time.Sleep(250 * time.Millisecond) + board.ClearScreen() + cprint.Printf["White"](" _______\n") + cprint.Printf["White"](" /\\ ()\\\n") + cprint.Printf["White"](" /()\\ () \\\n") + cprint.Printf["White"]("/ \\()_____\\\n") + cprint.Printf["White"]("\\ /() ()/\n") + cprint.Printf["White"](" \\()/ () /\n") + cprint.Printf["White"](" \\/()___()/\n") + time.Sleep(250 * time.Millisecond) + } + r := rand.Intn(6) + 1 + resultDice(r) + return r +} + +func resultDice(r int) { + switch r { + case 1: + board.ClearScreen() + cprint.Printf["White"](" ____________\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () |\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" |____________|\n") + time.Sleep(1 * time.Second) + case 2: + board.ClearScreen() + cprint.Printf["White"](" ____________\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () |\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () |\n") + cprint.Printf["White"](" |____________|\n") + time.Sleep(1 * time.Second) + case 3: + board.ClearScreen() + cprint.Printf["White"](" ____________\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () |\n") + cprint.Printf["White"](" | () |\n") + cprint.Printf["White"](" | () |\n") + cprint.Printf["White"](" |____________|\n") + time.Sleep(1 * time.Second) + case 4: + board.ClearScreen() + cprint.Printf["White"](" ____________\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () () |\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () () |\n") + cprint.Printf["White"](" |____________|\n") + time.Sleep(1 * time.Second) + case 5: + board.ClearScreen() + cprint.Printf["White"](" ____________\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () () |\n") + cprint.Printf["White"](" | () |\n") + cprint.Printf["White"](" | () () |\n") + cprint.Printf["White"](" |____________|\n") + time.Sleep(1 * time.Second) + case 6: + board.ClearScreen() + cprint.Printf["White"](" ____________\n") + cprint.Printf["White"](" | |\n") + cprint.Printf["White"](" | () () |\n") + cprint.Printf["White"](" | () () |\n") + cprint.Printf["White"](" | () () |\n") + cprint.Printf["White"](" |____________|\n") + time.Sleep(1 * time.Second) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b01d5b3 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/ghfarrell/go-battleship + +go 1.17 + +require ( + github.com/jwalton/go-supportscolor v1.1.0 // direct + golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 // indirect + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..84ec4fa --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +github.com/jwalton/go-supportscolor v1.1.0 h1:HsXFJdMPjRUAx8cIW6g30hVSFYaxh9yRQwEWgkAR7lQ= +github.com/jwalton/go-supportscolor v1.1.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4211b0e --- /dev/null +++ b/main.go @@ -0,0 +1,115 @@ +/* + TODO: + - give AI a hard mode + - make AI communicate with player (sassy) +*/ + +/* + COLORS: + - Cyan: Game talking to user + - Red: Errors or hit + - White/Yellow: Expecting input + - Green: Response to input + - Magenta: Static game text and game over text + - Blue: Debug and AI talking +*/ + +package main + +import ( + "flag" + "fmt" + "math" + "math/rand" + "os" + "os/signal" + "time" + + "github.com/ghfarrell/go-battleship/board" + "github.com/ghfarrell/go-battleship/cprint" + "github.com/ghfarrell/go-battleship/player" +) + +func PlayerGoesFirst() bool { + var playerGuess int + for { + cprint.Printf["Cyan"]("Time to determine who goes first! Guess a number 1-6: ") + _, err := fmt.Scanf("%d\n", &playerGuess) + if err != nil { + cprint.Printf["Red"]("Bad input! Try again\n") + } else { + break + } + } + aiGuess := rand.Intn(6) + 1 + cprint.Printf["Cyan"]("The AI made its guess! Time to see who is closest...\n") + time.Sleep(1 * time.Second) + r := RollTheBones() + fmt.Println() + cprint.Printf["Cyan"]("The number is %d!\n", r) + cprint.Printf["Cyan"]("The AI guessed %d, so the winner is...\n", aiGuess) + if math.Abs(float64(r-playerGuess)) < math.Abs(float64(r-aiGuess)) { + cprint.Printf["Cyan"]("You! You will make your strike first.\n") + } else if math.Abs(float64(r-playerGuess)) == math.Abs(float64(r-aiGuess)) { + cprint.Printf["Cyan"]("You... via tiebreaker!\nYou both guessed %d, but I like you more than the AI.\n", playerGuess) + } else { + cprint.Printf["Cyan"]("The AI! Too bad! They will strike first.\n") + } + time.Sleep(3 * time.Second) + return true +} + +func playerWin() { + +} + +func aiWin() { + +} + +func main() { + var ( + User player.Player + ai player.AI + userFirst bool + ) + debug := flag.Bool("debug", false, "Turns debug mode on") + flag.Parse() + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + <-c + cprint.Printf["Red"]("\nExiting Battleship... ") + os.Exit(1) + }() + User.Initialize() + ai.Initialize() + if *debug { + User.DebugPlaceShips() + cprint.Printf["Magenta"]("User ships placed...\n") + ai.PlaceShips() + cprint.Printf["Magenta"]("AI ships placed...\n") + userFirst = true + } else { + board.ClearScreen() + cprint.Printf["Cyan"]("Time to place your ships!\n\n") + User.PlaceShips() + ai.PlaceShips() + userFirst = PlayerGoesFirst() + } + //main gameplay loop + if userFirst { + for { + if User.Guess(&ai, *debug) || ai.Guess(&User, *debug) { + break + } + + } + } else { + for { + if ai.Guess(&User, *debug) || User.Guess(&ai, *debug) { + break + } + } + } +} diff --git a/player/player.go b/player/player.go new file mode 100644 index 0000000..f841fb8 --- /dev/null +++ b/player/player.go @@ -0,0 +1,299 @@ +package player + +import ( + "fmt" + "math/rand" + "time" + + "github.com/ghfarrell/go-battleship/board" + "github.com/ghfarrell/go-battleship/cprint" +) + +type Direction int + +const ( + Up Direction = iota + Right + Down + Left +) + +func (d Direction) opposite() Direction { + switch d { + case Up: + return Down + case Down: + return Up + case Left: + return Right + case Right: + return Left + default: + //impossible but compiler yells at me if i dont + return Up + } +} + +/* + In ship slice, ships are ordered by length, longest to smallest, and alphabetically +*/ +type Player struct { + Guesses []board.Point + Friendly board.Board + Enemy board.Board +} + +type AI struct { + // a Column value of -1 means the pivot is not in use + Pivot board.Point + PivotOffset int + Dir Direction + Player +} + +func (p *Player) Initialize() { + p.Friendly.Initialize() + p.Enemy.Initialize() + p.Guesses = make([]board.Point, 0) +} +func (p *AI) Initialize() { + p.Friendly.Initialize() + p.Enemy.Initialize() + p.Guesses = make([]board.Point, 0) + p.PivotOffset = 1 + p.Pivot = board.Point{ + Row: 'a', + Col: -1, + } + p.Dir = Up +} + +func (p Player) PrintFriendly() { + fmt.Println(" 1 2 3 4 5 6 7 8 9 10") + for c := 'a'; c <= 'j'; c++ { + fmt.Printf("%c", c) + for i := 1; i <= 10; i++ { + cprint.Printf["White"](" " + string(p.Friendly.Board[board.CoordToPoint(c, i)])) + } + fmt.Println() + } +} +func (p Player) PrintEnemy() { + fmt.Println(" 1 2 3 4 5 6 7 8 9 10") + for c := 'a'; c <= 'j'; c++ { + fmt.Printf("%c", c) + for i := 1; i <= 10; i++ { + cprint.Printf["Red"](" " + string(p.Enemy.Board[board.CoordToPoint(c, i)])) + } + fmt.Println() + } +} + +func hitResponse() string { + r := []string{ + "A perfect shot!", + "Nice hit!", + "Dead on!", + "Incredible shot!", + "Nice one!", + } + return r[rand.Intn(len(r))] +} +func missResponse() string { + r := []string{ + "So close!", + "Try hitting a boat next time...", + "Almost!", + "Too bad!", + "No dice!", + } + return r[rand.Intn(len(r))] +} + +func (p Player) hasGuessed(c board.Point) bool { + for _, i := range p.Guesses { + if i == c { + return true + } + } + return false +} + +func (p Player) DebugPrintGuesses() { + cprint.Printf["Blue"]("[") + for _, c := range p.Guesses { + cprint.Printf["Blue"](" %c%d ", c.Row, c.Col) + } + cprint.Printf["Blue"]("]") +} + +func (p *Player) PlaceShips() { + p.Friendly.PlaceShips() +} +func (p *Player) DebugPlaceShips() { + p.Friendly.AutoPlaceShips() +} + +func (a *AI) PlaceShips() { + a.Friendly.AutoPlaceShips() +} + +func (p *Player) Guess(a *AI, debug bool) bool { + var ( + c board.Point + err error + ) + board.ClearScreen() + p.PrintEnemy() + p.PrintFriendly() + if debug { + a.PrintFriendly() + } + if debug { + cprint.Printf["Blue"]("DEBUG: USER GUESSES -> ") + p.DebugPrintGuesses() + cprint.Printf["Blue"]("\nDEBUG: AI GUESSES -> ") + a.DebugPrintGuesses() + fmt.Println() + } + for { + cprint.Printf["Yellow"]("Pick a coordinate to attack: ") + c, err = board.InputPoint() + if err != nil { + cprint.Printf["Red"]("Error: " + err.Error() + "\n") + } else if p.hasGuessed(c) { + cprint.Printf["Red"]("Woops! You already guessed that spot.\n") + } else { + p.Guesses = append(p.Guesses, c) + break + } + } + if hit, ship := a.Friendly.CheckForHit(c); hit { + a.Friendly.Board[c] = 'X' + p.Enemy.Board[c] = 'X' + cprint.Printf["Green"](hitResponse() + "\n") + cprint.Printf["Green"]("You hit the AI's %s!\n", ship.Name) + if a.Friendly.Sunk(ship) { + cprint.Printf["Green"]("The AI's %s sunk!\n", ship.Name) + } + if a.Friendly.GameOver() { + cprint.Printf["Magenta"]("YOU WIN! THE EVIL AI IS DEFEATED!\n") + return true + } + } else { + p.Enemy.Board[c] = 'O' + cprint.Printf["Red"](missResponse() + "\n") + } + time.Sleep(2 * time.Second) + return false +} + +func (a AI) randomGuess() board.Point { + for { + c := board.RandPoint() + if !a.hasGuessed(c) { + return c + } + } +} + +/* + guessing alg should work like: + - randomly guess a spot until you hit, that spot is the pivot + - go one direction (say, up) until either you miss or you sink + - if you miss, go one spot below the pivot + - if that misses, go left from the pivot + - if that misses, go right from the pivot + - since you go all directions, one must work and you sink a ship + - when you sink, go back to random guesses and repeat +*/ + +/* + TODO: + - it guessed up and missed, so when it guessed right and hit, it guessed that + exact same spot again which is impossible to guess the same spot twice... +*/ +func (a *AI) getGuess() (c board.Point) { + if a.Pivot.Col == -1 || len(a.Guesses) < 1 { + c = a.randomGuess() + return + } + switch a.Dir { + case Up: + c = a.Pivot.NudgeUp(a.PivotOffset) + case Down: + c = a.Pivot.NudgeDown(a.PivotOffset) + case Left: + c = a.Pivot.NudgeLeft(a.PivotOffset) + case Right: + c = a.Pivot.NudgeRight(a.PivotOffset) + } + if a.hasGuessed(c) { // if the bot somehow comes up with a point that has already been guessed, + c = a.randomGuess() // ill deal with it later + } else if c.IsValid() { + a.PivotOffset++ + return + } else { // if the nudged coordinate is out of bounds, go the opposite way + a.PivotOffset = 1 + a.Dir = a.Dir.opposite() + c = a.getGuess() + } + return +} + +func (a *AI) Guess(p *Player, debug bool) bool { + c := a.getGuess() + a.Guesses = append(a.Guesses, c) + cprint.Printf["White"]("Now the AI's time to strike...\n") + if !debug { + time.Sleep(1 * time.Second) + } + cprint.Printf["White"]("The AI has fired at coordinate %c%d!\n", c.Row, c.Col) + if !debug { + time.Sleep(1 * time.Second) + } + if hit, sunkShip := p.Friendly.CheckForHit(c); hit { + cprint.Printf["Red"]("You've been hit!\n") + p.Friendly.Board[c] = 'X' + if a.Pivot.Col == -1 { // if AI wasn't guessing on a pivot and hit, + a.Pivot = c // set the new pivot + a.PivotOffset = 1 + a.Dir = Up + if debug { + cprint.Printf["Blue"]("DEBUG: New pivot: %c%d\n", a.Pivot.Row, a.Pivot.Col) + } + } // else, keep on guessing on that pivot + cprint.Printf["Red"]("Your %s has taken a hit!\n", sunkShip.Name) + if p.Friendly.Sunk(sunkShip) { + // a little more complex case: if the AI was guessing on a line and sunk a ship that is + // shorter than the line it was guessing on, then the line must be composed of multiple + // ships, so after sinking one of the two ships, it will travel in the opposite direction + // to sink the other ship that composed that line + if a.PivotOffset > sunkShip.Length { + a.PivotOffset = 1 + a.Dir = a.Dir.opposite() + } else { + // reset pivot + a.Pivot.Col = -1 + cprint.Printf["Red"]("The AI has sunk your %s!\n", sunkShip.Name) + } + } + if p.Friendly.GameOver() { + cprint.Printf["Magenta"]("OH GLOB! YOU LOST! YOU STINK!\n") + return true + + } + } else { + if a.Pivot.Col != -1 { // if AI was guessing on a pivot and missed... + if a.PivotOffset > 2 { // if the AI was following a track of hits and missed + a.Dir = a.Dir.opposite() // go the opposite direction + } else { // cycle directions + a.Dir++ + } + a.PivotOffset = 1 + } + cprint.Printf["Green"]("Phew! The AI doesn't have very good aim.") + } + time.Sleep(3 * time.Second) + return false +} diff --git a/tests/board_test.go b/tests/board_test.go new file mode 100644 index 0000000..45d3b40 --- /dev/null +++ b/tests/board_test.go @@ -0,0 +1,66 @@ +package board_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/ghfarrell/go-battleship/board" + "github.com/ghfarrell/go-battleship/cprint" +) + +var b board.Board + +func init() { + rand.Seed(int64(time.Now().Nanosecond())) + b.Board = make(map[board.Point]byte, 100) + for c := 'a'; c <= 'j'; c++ { + for i := 1; i <= 10; i++ { + b.Board[board.CoordToPoint(c, i)] = '-' + } + } + b.Ships = make([]board.Ship, 5) + b.Ships[0].Name = "Carrier" + b.Ships[1].Name = "Battleship" + b.Ships[2].Name = "Cruiser" + b.Ships[3].Name = "Submarine" + b.Ships[4].Name = "Destroyer" + b.Ships[0].Length = 5 + b.Ships[1].Length = 4 + b.Ships[2].Length = 3 + b.Ships[3].Length = 3 + b.Ships[4].Length = 2 + for _, s := range b.Ships { + s.Coords = make([]board.Point, 0) + } +} +func TestPlaceShip(t *testing.T) { + s := b.Ships[0] + testPoint := board.Point{ + Row: 'a', + Col: 1, + } + solPoints := make([]board.Point, 0) + solPoints = append(solPoints, board.Point{Row: 'a', Col: 1}) + solPoints = append(solPoints, board.Point{Row: 'b', Col: 1}) + solPoints = append(solPoints, board.Point{Row: 'c', Col: 1}) + solPoints = append(solPoints, board.Point{Row: 'd', Col: 1}) + solPoints = append(solPoints, board.Point{Row: 'e', Col: 1}) + s, _ = b.PlaceShip(s, testPoint, true) + cprint.Printf["Red"]("%s: %v\n", s.Name, s.Coords) + for i := 0; i < 5; i++ { + cprint.Printf["Red"]("%v\n", s.Coords[i]) + if s.Coords[i] != solPoints[i] { + t.Errorf("Wrong points; Expected %v got %v", solPoints, s.Coords) + } + } +} + +func TestAutoPlaceShips(t *testing.T) { + b.AutoPlaceShips() + for _, s := range b.Ships { + if len(s.Coords) < 1 { + t.Errorf("Expect coordinate array, got %v", s.Coords) + } + } +}