๐Ÿ“ฆ sleepyfran / duets

๐Ÿ“„ Car.Tests.fs ยท 295 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295module Duets.Simulation.Tests.Car

open Duets.Data.Items
open Duets.Data.World
open Duets.Simulation.Vehicles
open NUnit.Framework
open FsUnit
open Test.Common.Generators

open Duets.Common
open Duets.Entities
open Duets.Simulation

let currentPlace =
    Queries.World.placesByTypeInCity Prague PlaceTypeIndex.Restaurant
    |> List.find (fun place -> place.Name = "Bistro Stromovka")

let currentStreet =
    currentPlace.Exits |> Map.head |> Queries.World.streetById Prague

let state =
    State.generateOne State.defaultOptions
    |> State.World.move Prague currentPlace.Id Ids.Common.bar

let testCar, _ = Vehicles.Car.toyotaCorolla

let carPosition = (Prague, currentStreet.Id, "0")

(* == Within city drive tests == *)

[<Test>]
let ``planWithinCityDrive returns AlreadyAtDestination when already at destination``
    ()
    =
    let currentPlace = Queries.World.currentPlace state

    let result = Car.planWithinCityDrive state currentPlace

    match result with
    | Error Car.AlreadyAtDestination -> ()
    | _ -> failwith "Expected AlreadyAtDestination error"

[<Test>]
let ``planWithinCityDrive returns CannotReachDestination when no path exists``
    ()
    =
    let placeInDifferentCity =
        Queries.World.placesByTypeInCity London PlaceTypeIndex.Hotel
        |> List.head

    let result = Car.planWithinCityDrive state placeInDifferentCity

    match result with
    | Error Car.CannotReachDestination -> ()
    | _ -> failwith "Expected CannotReachDestination error"

[<Test>]
let ``planWithinCityDrive returns success with path and travel time`` () =
    let destination =
        Queries.World.placesByTypeInCity Prague PlaceTypeIndex.Airport
        |> List.head

    let result = Car.planWithinCityDrive state destination

    match result with
    | Ok(path, travelTime) ->
        path |> should not' (be Empty)
        travelTime |> should be (greaterThan 0<minute>)
    | Error _ -> failwith "Expected successful planning"

[<Test>]
let ``planWithinCityDrive travel time is faster than public transport`` () =
    let destination =
        Queries.World.placesByTypeInCity Prague PlaceTypeIndex.Airport
        |> List.head

    let currentPlace = Queries.World.currentPlace state
    let currentCity = Queries.World.currentCity state

    let path =
        Navigation.Pathfinding.directionsToNode
            currentCity.Id
            currentPlace.Id
            destination.Id

    match path, Car.planWithinCityDrive state destination with
    | Some pathActions, Ok(_, carTravelTime) ->
        let publicTransportTime =
            Navigation.TravelTime.byPublicTransport pathActions

        carTravelTime |> should be (lessThan publicTransportTime)
    | _ -> failwith "Expected valid path and successful planning"

[<Test>]
let ``driveWithinCity moves character near destination place`` () =
    let destination =
        Queries.World.placesByTypeInCity Prague PlaceTypeIndex.ConcertSpace
        |> List.sample

    let effects = Car.driveWithinCity state destination carPosition testCar

    effects |> should not' (be Empty)

    effects
    |> List.exists (function
        | WorldMoveToPlace(Diff(_, (_, newPlaceId, _))) ->
            destination.Exits |> Map.head = newPlaceId
        | _ -> false)
    |> should be True

[<Test>]
let ``driveWithinCity removes car from old position and adds to new position``
    ()
    =
    let destination =
        Queries.World.placesByTypeInCity Prague PlaceTypeIndex.Airport
        |> List.head

    let effects = Car.driveWithinCity state destination carPosition testCar

    effects
    |> List.exists (function
        | ItemRemovedFromWorld(pos, car) -> pos = carPosition && car = testCar
        | _ -> false)
    |> should be True

    effects
    |> List.exists (function
        | ItemAddedToWorld(_, car) -> car = testCar
        | _ -> false)
    |> should be True

(* == Intercity drive tests == *)

[<Test>]
let ``planIntercityDrive returns AlreadyAtDestination when already in destination city``
    ()
    =
    let currentCityId, _, _ = Queries.World.currentCoordinates state

    let result = Car.planIntercityDrive state currentCityId testCar

    match result with
    | Error Car.AlreadyAtDestination -> ()
    | _ -> failwith "Expected AlreadyAtDestination error"

[<Test>]
let ``planIntercityDrive returns CannotReachDestination when no road connection exists``
    ()
    =
    // Prague to New York has no road connection (only air).
    let result = Car.planIntercityDrive state NewYork testCar

    match result with
    | Error Car.CannotReachDestination -> ()
    | _ -> failwith "Expected CannotReachDestination error"

[<Test>]
let ``planIntercityDrive returns success with distance and travel time for connected cities``
    ()
    =
    // Prague to London is connected by road (1035 km).
    let result = Car.planIntercityDrive state London testCar

    match result with
    | Ok(distance, travelTimeHours, dayMoments) ->
        distance |> should equal 1035<km>
        travelTimeHours |> should be (greaterThan 0<hour>)
        dayMoments |> should be (greaterThan 0<dayMoments>)
    | Error err -> failwith $"Expected successful planning, but got {err}"

[<Test>]
let ``planIntercityDrive calculates travel time based on car power`` () =
    let lowPowerCar, _ = Vehicles.Car.toyotaCorolla // 169 HP.
    let highPowerCar, _ = Vehicles.Car.porsche911 // 443 HP.

    let lowPowerResult = Car.planIntercityDrive state London lowPowerCar
    let highPowerResult = Car.planIntercityDrive state London highPowerCar

    match lowPowerResult, highPowerResult with
    | Ok(_, lowPowerTime, _), Ok(_, highPowerTime, _) ->
        // Higher power car should be faster.
        highPowerTime |> should be (lessThanOrEqualTo lowPowerTime)
    | _ -> failwith "Expected successful planning for both cars"

[<Test>]
let ``planIntercityDrive travel time scales with distance`` () =
    // Madrid is 1780 km from Prague.
    let longDistanceResult = Car.planIntercityDrive state Madrid testCar
    // London is 1035 km from Prague.
    let shortDistanceResult = Car.planIntercityDrive state London testCar

    match longDistanceResult, shortDistanceResult with
    | Ok(longDistance, longTime, _), Ok(shortDistance, shortTime, _) ->
        longDistance |> should be (greaterThan shortDistance)
        longTime |> should be (greaterThan shortTime)
    | _ -> failwith "Expected successful planning for both destinations"

[<Test>]
let ``driveToCity moves character to first street of destination city`` () =
    let destinationCityId = London
    let tripDuration = 10<dayMoments>

    let effects =
        Car.driveToCity state destinationCityId carPosition testCar tripDuration

    effects
    |> List.exists (function
        | WorldMoveToPlace(Diff(_, (cityId, _, _))) ->
            cityId = destinationCityId
        | _ -> false)
    |> should be True

[<Test>]
let ``driveToCity removes car from old position and adds to new city`` () =
    let destinationCityId = London
    let tripDuration = 10<dayMoments>

    let effects =
        Car.driveToCity state destinationCityId carPosition testCar tripDuration

    effects
    |> List.exists (function
        | ItemRemovedFromWorld(pos, car) -> pos = carPosition && car = testCar
        | _ -> false)
    |> should be True

    effects
    |> List.exists (function
        | ItemAddedToWorld((cityId, _, _), car) ->
            cityId = destinationCityId && car = testCar
        | _ -> false)
    |> should be True

[<Test>]
let ``driveToCity advances time by trip duration`` () =
    let destinationCityId = London
    let tripDuration = 10<dayMoments>

    let effects =
        Car.driveToCity state destinationCityId carPosition testCar tripDuration

    let advanceTimeEffects =
        effects
        |> List.filter (function
            | TimeAdvanced _ -> true
            | _ -> false)

    advanceTimeEffects |> should not' (be Empty)

[<Test>]
let ``driveToCity refreshes character attributes during trip`` () =
    let destinationCityId = Madrid
    let tripDuration = 15<dayMoments>

    let effects =
        Car.driveToCity state destinationCityId carPosition testCar tripDuration

    // Should have attribute refresh effects to prevent character depletion.
    effects
    |> List.exists (function
        | CharacterAttributeChanged(_, CharacterAttribute.Energy, _) -> true
        | _ -> false)
    |> should be True

    effects
    |> List.exists (function
        | CharacterAttributeChanged(_, CharacterAttribute.Hunger, _) -> true
        | _ -> false)
    |> should be True

    effects
    |> List.exists (function
        | CharacterAttributeChanged(_, CharacterAttribute.Mood, _) -> true
        | _ -> false)
    |> should be True

[<Test>]
let ``driveToCity generates one refresh per day moment`` () =
    let destinationCityId = London
    let tripDuration = 5<dayMoments>

    let effects =
        Car.driveToCity state destinationCityId carPosition testCar tripDuration

    let advanceTimeEffects =
        effects
        |> List.filter (function
            | TimeAdvanced _ -> true
            | _ -> false)
        |> List.length

    // Should have one AdvancedTime effect per day moment.
    advanceTimeEffects |> should equal 5