You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
6.9 KiB
303 lines
6.9 KiB
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 {
|
|
board.ClearScreen()
|
|
p.PrintEnemy()
|
|
p.PrintFriendly()
|
|
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
|
|
}
|