I'm trying to wrap my head around F#. I'd like your honest opinion about this simple game source code that I wrote. Rules are simple:
- Every player has soldiers and territory
- Player can recruit soldiers, gain territory or attack another player
- Player loses when he has no territory, wins when everybody else loses
My main problem is still code organization and usage of mutable fields. I believe it can be done better, but I don't really know how.
Config module:
[<AutoOpen>]
module Config
let humanPlayers = 1
let aiPlayers = 2
let aiComputationSleep = 200
let aiAttacksWhenHaveXSoldiers = 1500
let minInitialSoldiers = 750
let maxInitialSoldiers = 1000
let minInitialArea = 750
let maxInitialArea = 1000
let clearConsoleAfterEachTurn = true
Utils module:
[<AutoOpen>]
module Utils
open System
let subToZero a b =
match a >= b with
| true -> a - b
| false -> 0
let random = new Random()
let rand (a : int) (b : int) = random.Next(Math.Min(a, b), Math.Max(a, b))
let single f list =
let matches = list |> List.filter f
match matches.Length with
| 1 -> Some (List.head matches)
| _ -> None
Types module:
[<AutoOpen>]
module Types
type PlayerType =
| Human
| AI
type Player(name : string, playerType : PlayerType, soldiers : int, area : int) =
member this.Name = name
member this.PlayerType = playerType
member val Soldiers = soldiers with get, set
member val Area = area with get, set
member this.NameWithType = sprintf "%s(%A)" this.Name this.PlayerType
member this.Alive() = this.Area > 0
type GameState =
{ turn : int
players : Player list
winner : Player option }
type PlayerMove =
| Recruit
| GainTerritory
| Attack of Player
| NotProvided
type AttackResult =
{ attacker : Player
attackerLoses : int
defender : Player
defenderLoses : int
area : int }
type MoveResult =
| Recruted of Player * int
| TerritoryGained of Player * int
| Attacked of AttackResult
| Unknown
type GetMove = Player list -> Player -> PlayerMove
type MoveDispatcher(humanMove: GetMove, aiMove: GetMove) =
member this.HumanMove = humanMove
member this.AIMove = aiMove
member this.GetMove (players : Player list) (player : Player) =
match player.PlayerType with
| Human -> this.HumanMove players player
| AI -> this.AIMove players player
Init module:
module Init
let initialSoldiers() = rand minInitialSoldiers maxInitialSoldiers
let initialArea() = rand minInitialArea maxInitialArea
let initPlayer (playerType : PlayerType) (num : int) =
Player("Player" + num.ToString(), playerType, initialSoldiers(), initialArea())
let initPlayers =
let human = List.init humanPlayers (initPlayer Human)
let ai = List.init aiPlayers (initPlayer AI)
human @ ai
let initGame =
{ turn = 1
players = initPlayers
winner = None }
AI module:
module AI
open System.Threading
let getEnemyToAttackForAI (players : Player list) (player : Player) =
players
|> List.except [ player ]
|> List.maxBy (fun x -> x.Area)
let getAIMove (players : Player list) (player : Player) =
Thread.Sleep(aiComputationSleep)
if player.Soldiers > aiAttacksWhenHaveXSoldiers then Attack(getEnemyToAttackForAI players player)
elif player.Soldiers > player.Area then GainTerritory
else Recruit
GameLogic module:
module GameLogic
open System
let recruitSoldiers (player : Player) =
let recruits = rand 100 player.Area
player.Soldiers <- player.Soldiers + recruits
Recruted(player, recruits)
let gainTerritory (player : Player) =
let gainedArea = rand 100 player.Soldiers
player.Area <- player.Area + gainedArea
TerritoryGained(player, gainedArea)
let getNextState state : GameState =
{ state with turn = state.turn + 1
winner = None }
let attack (player : Player) (enemy : Player) =
let attackerKills = rand 0 player.Soldiers
enemy.Soldiers <- subToZero enemy.Soldiers attackerKills
let defenderKills = rand 0 enemy.Soldiers
player.Soldiers <- subToZero player.Soldiers defenderKills
let areaConquered = subToZero attackerKills defenderKills
player.Area <- player.Area + areaConquered
enemy.Area <- subToZero enemy.Area areaConquered
Attacked { attacker = player
attackerLoses = defenderKills
defender = enemy
defenderLoses = attackerKills
area = areaConquered }
let changePlayerState playerMove (player : Player) =
match playerMove with
| Recruit -> recruitSoldiers player
| GainTerritory -> gainTerritory player
| Attack enemy -> attack player enemy
| NotProvided -> Unknown
let checkIfGameFinished (state : GameState) (players : Player list) =
match players |> single (fun x -> x.Alive()) with
| Some player -> { state with winner = Some player }
| None -> state
ConsoleUi module:
module ConsoleUi
open System
open GameLogic
let pause() = Console.Read() |> ignore
let printTitle() = printfn "SmallWars!"
let printHeader() =
if clearConsoleAfterEachTurn then Console.Clear()
printTitle()
printfn "------------------------------------------------------------------------"
let printPlayerState (player : Player) =
printfn "%s kingdom have %i soldiers and %i area" player.NameWithType player.Soldiers player.Area
let printState (state : GameState) =
match state.winner with
| None ->
printfn "Turn %i" state.turn
state.players |> List.iter printPlayerState
| Some player ->
printfn "Game is finished! %s won!" player.NameWithType
pause()
let printMoveResult =
function
| Recruted(player, soldiers) -> printfn "%s recruted %i soldiers" player.NameWithType soldiers
| TerritoryGained(player, area) -> printfn "%s gained %i area" player.NameWithType area
| Attacked result ->
printfn "%s attacked %s. Loses %i - %i. Area conquered %i" result.attacker.NameWithType
result.defender.NameWithType result.attackerLoses result.defenderLoses result.area
| Unknown -> printfn ""
let printEmptyLine() = printfn ""
let getPossibleTargets (players : Player list) (player : Player) = players |> List.except [ player ]
let getEnemyToAttackForHuman (players : Player list) (player : Player) =
printfn "Who you want to attack?"
let targets = getPossibleTargets players player
targets |> List.iteri (fun i x -> printfn "%i - %s" i x.NameWithType)
match Int32.TryParse(Console.ReadLine()) with
| true, i when i < List.length targets -> Attack targets.[i]
| _, _ -> NotProvided
let getHumanMove (players : Player list) (player : Player) =
match Console.ReadLine() with
| "r" -> Recruit
| "g" -> GainTerritory
| "a" -> getEnemyToAttackForHuman players player
| _ -> NotProvided
Program.fs - entry point:
open Init
open GameLogic
open ConsoleUi
open AI
[<EntryPoint>]
let main argv =
let mutable state = initGame
let mutable moveResult = Unknown
let moveDispatcher = MoveDispatcher(getHumanMove, getAIMove)
let movePlayer players player =
printHeader()
printMoveResult moveResult
printEmptyLine()
printState state
let playerInput = moveDispatcher.GetMove players player
moveResult <- changePlayerState playerInput player
state <- getNextState state
while state.winner.IsNone do
state.players
|> List.filter (fun x-> x.Alive())
|> List.iter (fun x -> movePlayer state.players x)
state <- checkIfGameFinished state state.players
printEmptyLine()
printMoveResult moveResult
printState state
pause()
0
while
andfor
loops intorec
ursive functions that pass the changing state to themselves does help. That could get rid of the properties in typePlayer
, too. \$\endgroup\$