Inspired by this question, I went on with the following version. My goal was to focus on pure functions, to avoid mutable variables and to be strictly functional.
I have ignored the computer player part - as it will not change the overall design.
An issue may be that an unchanged player survives from one game state to the next. That's maybe not a good idea, but will it work here because game states are not used anywhere else?
module BattleShipGame
open System
let boardSize = 10
type Direction = Horz | Vert
type ShipType =
| AircraftCarrier
| BattleShip
| Frigate
| Submarine
| Minesweeper
type Point(x : int, y : int) =
struct
member this.X = x
member this.Y = y
end
type Ship(position : Point, shipType : ShipType, direction : Direction, hits : Point list) =
member this.Position = position
member this.Type = shipType
member this.Direction = direction
member this.Hits = hits
member this.Length =
match this.Type with
| AircraftCarrier -> 5
| BattleShip -> 4
| Frigate | Submarine -> 3
| Minesweeper -> 2
member this.IsFinished =
this.Hits.Length >= this.Length
member this.HitTest(pos : Point) =
let offset, dirValid = if this.Direction = Horz then pos.X - this.Position.X, pos.Y = this.Position.Y else pos.Y - this.Position.Y, pos.X = this.Position.X
if not dirValid then
false
else
offset >= 0 && offset < this.Length
static member Copy(ship: Ship, hit : Point) =
Ship(ship.Position, ship.Type, ship.Direction, hit :: ship.Hits)
type Player(name : string, ships : Ship list, enemyHits : Point list) =
member this.Name = name
member this.Ships = ships
member this.EnemyHits = enemyHits;
member this.IsFinished = (this.Ships |> List.tryFind (fun sh -> sh.IsFinished = false) ).IsNone
member this.Print() =
let shipHits = this.Ships |> List.collect (fun sh -> sh.Hits)
printf "y\x"
for x in 1 .. boardSize do
printf " %d " x
printfn ""
for y in 1 .. boardSize do
for x in 0 .. boardSize do
let hit = shipHits |> List.tryFind (fun pt -> pt.X = x && pt.Y = y)
if x = 0 && y > 0 then
printf "%3d" y
elif hit.IsSome then
printf " X "
else
let pt = this.EnemyHits |> List.tryFind (fun pt -> pt.X = x && pt.Y = y)
if pt.IsSome then
printf " 0 "
else
printf " ";
Console.WriteLine()
static member Copy(player : Player, hittedShip : Ship option, hit : Point) =
if hittedShip.IsSome then
let shipList = Ship.Copy(hittedShip.Value, hit) :: player.Ships |> List.where (fun sh -> sh <> hittedShip.Value)
Player(player.Name, shipList, player.EnemyHits)
else
Player(player.Name, player.Ships, hit :: player.EnemyHits)
// The current state of the game
type GameState(currentPlayer : Player, otherPlayer : Player) =
member this.CurrentPlayer = currentPlayer
member this.OtherPlayer = otherPlayer
member this.IsFinished = currentPlayer.IsFinished || otherPlayer.IsFinished
// Prompts for a point in the board coordinate system x = horz, y = vert
let getUserTarget() =
let rec gut() =
Console.Write("Enter target [x,y]:")
let input = Console.ReadLine().Split(',')
if input.Length <> 2 then
gut()
else
let mutable x = 0
let mutable y = 0
if Int32.TryParse(input.[0], &x) && Int32.TryParse(input.[1], &y) then
if x < 1 || x > boardSize || y < 1 || y > boardSize then
gut()
else
Point(x, y)
else
gut()
gut()
// Prints name of the current player and the board of the other player (which is the current players view of state)
let printBoard (state : GameState) =
Console.Clear()
printfn "Current Player: %s:" state.CurrentPlayer.Name
state.OtherPlayer.Print()
// The "main" function which is repeatedly called by Seq.unfold below in main
let getState (state : GameState) =
if state.IsFinished then
printBoard state
None
else
printBoard state
let target = getUserTarget()
let hittest = state.OtherPlayer.Ships |> List.tryFind (fun ship -> ship.HitTest target)
if hittest.IsSome then
Some((state, GameState(state.CurrentPlayer, Player.Copy(state.OtherPlayer, hittest, target))))
else
Some((state, GameState(Player.Copy(state.OtherPlayer, None, target), state.CurrentPlayer)))
let initPlayer name =
// TODO: change to user input loop including all ship types
if name = "Hillary" then
let ships = [
Ship(Point(1, 1), AircraftCarrier, Horz, []);
Ship(Point(5, 3), BattleShip, Vert, []);
Ship(Point(8, 9), Frigate, Horz, [])
]
Player(name, ships, [])
else
let ships = [
Ship(Point(1, 1), AircraftCarrier, Horz, []);
Ship(Point(5, 3), BattleShip, Vert, []);
Ship(Point(8, 9), Frigate, Horz, [])
]
Player(name, ships, [])
[<EntryPoint>]
let main argv =
// Creates a sequence of game states of which the last holds the winner as the CurrentPlayer
let result = Seq.unfold (fun state -> getState state) (GameState(initPlayer("Hillary"), initPlayer("Donald")))
result
|> Seq.last
|> fun state -> (printfn "\nWinner: %s" state.CurrentPlayer.Name)
Console.WriteLine("END")
Console.ReadLine() |> ignore
0