Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign collision handling #145

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion elm.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"dependencies": {
"direct": {
"Elm-Canvas/raster-shapes": "1.1.2",
"Janiczek/elm-list-cartesian": "1.0.2",
"avh4/elm-color": "1.0.0",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
Expand Down
4 changes: 2 additions & 2 deletions src/Canvas.elm
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ headDrawingCmd =
renderOverlay
<< List.map
(\kurve ->
{ position = World.drawingPosition kurve.state.position
{ position = World.drawingPosition (World.toPixel kurve.state.position)
, thickness = theThickness
, color = Color.toCssString kurve.color
}
Expand All @@ -53,7 +53,7 @@ drawSpawnIfAndOnlyIf shouldBeVisible kurve =
let
drawingPosition : DrawingPosition
drawingPosition =
World.drawingPosition kurve.state.position
World.drawingPosition (World.toPixel kurve.state.position)
in
if shouldBeVisible then
render <|
Expand Down
146 changes: 98 additions & 48 deletions src/Game.elm
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Random
import Round exposing (Kurves, Round, RoundInitialState, modifyAlive, modifyDead, roundIsOver)
import Set exposing (Set)
import Spawn exposing (generateHoleSize, generateHoleSpacing, generateKurves)
import Thickness exposing (theThickness)
import Thickness exposing (minimumDistanceFor45DegreeDraws, theThickness)
import Turning exposing (computeAngleChange, computeTurningState, turningStateFromHistory)
import Types.Angle as Angle exposing (Angle)
import Types.Distance as Distance exposing (Distance(..))
Expand All @@ -35,7 +35,7 @@ import Types.Speed as Speed
import Types.Tick as Tick exposing (Tick)
import Types.Tickrate as Tickrate
import Types.TurningState exposing (TurningState)
import World exposing (DrawingPosition, Pixel, Position, distanceToTicks)
import World exposing (DrawingPosition, Pixel, Position, distanceBetween, distanceToTicks, toPixel)


type GameState
Expand Down Expand Up @@ -125,7 +125,7 @@ prepareRoundFromKnownInitialState initialState =
round : Round
round =
{ kurves = { alive = theKurves, dead = [] }
, occupiedPixels = List.foldl (.state >> .position >> World.drawingPosition >> World.pixelsToOccupy >> Set.union) Set.empty theKurves
, occupiedPixelPositions = List.foldl (.state >> .position >> toPixel >> Set.insert) Set.empty theKurves
, initialState = initialState
, seed = initialState.seedAfterSpawn
}
Expand All @@ -136,25 +136,29 @@ prepareRoundFromKnownInitialState initialState =
reactToTick : Config -> Tick -> MidRoundState -> ( TickResult, Cmd msg )
reactToTick config tick (( _, currentRound ) as midRoundState) =
let
( newKurvesGenerator, newOccupiedPixels, newColoredDrawingPositions ) =
( newKurvesGenerator, newOccupiedPixelPositions, newColoredPixelPositions ) =
List.foldr
(checkIndividualKurve config tick)
( Random.constant
{ alive = [] -- We start with the empty list because the new one we'll create may not include all the Kurves from the old one.
, dead = currentRound.kurves.dead -- Dead Kurves, however, will not spring to life again.
}
, currentRound.occupiedPixels
, currentRound.occupiedPixelPositions
, []
)
currentRound.kurves.alive

( newKurves, newSeed ) =
Random.step newKurvesGenerator currentRound.seed

newColoredDrawingPositions : List ( Color, DrawingPosition )
newColoredDrawingPositions =
List.map (Tuple.mapSecond World.drawingPosition) newColoredPixelPositions

newCurrentRound : Round
newCurrentRound =
{ kurves = newKurves
, occupiedPixels = newOccupiedPixels
, occupiedPixelPositions = newOccupiedPixelPositions
, initialState = currentRound.initialState
, seed = newSeed
}
Expand Down Expand Up @@ -196,31 +200,31 @@ checkIndividualKurve :
Config
-> Tick
-> Kurve
-> ( Random.Generator Kurves, Set World.Pixel, List ( Color, DrawingPosition ) )
-> ( Random.Generator Kurves, Set World.Pixel, List ( Color, Pixel ) )
->
( Random.Generator Kurves
, Set World.Pixel
, List ( Color, DrawingPosition )
, List ( Color, Pixel )
)
checkIndividualKurve config tick kurve ( checkedKurvesGenerator, occupiedPixels, coloredDrawingPositions ) =
checkIndividualKurve config tick kurve ( checkedKurvesGenerator, occupiedPixelPositions, coloredPositions ) =
let
turningState : TurningState
turningState =
turningStateFromHistory tick kurve

( newKurveDrawingPositions, checkedKurveGenerator, fate ) =
updateKurve config turningState occupiedPixels kurve
( newKurvePixelPositions, checkedKurveGenerator, fate ) =
updateKurve config turningState occupiedPixelPositions kurve

occupiedPixelsAfterCheckingThisKurve : Set Pixel
occupiedPixelsAfterCheckingThisKurve =
occupiedPixelPositionsAfterCheckingThisKurve : Set Pixel
occupiedPixelPositionsAfterCheckingThisKurve =
List.foldl
(World.pixelsToOccupy >> Set.union)
occupiedPixels
newKurveDrawingPositions
Set.insert
occupiedPixelPositions
newKurvePixelPositions

coloredDrawingPositionsAfterCheckingThisKurve : List ( Color, DrawingPosition )
coloredDrawingPositionsAfterCheckingThisKurve =
coloredDrawingPositions ++ List.map (Tuple.pair kurve.color) newKurveDrawingPositions
coloredPixelPositionsAfterCheckingThisKurve : List ( Color, Pixel )
coloredPixelPositionsAfterCheckingThisKurve =
coloredPositions ++ List.map (Tuple.pair kurve.color) newKurvePixelPositions

kurvesAfterCheckingThisKurve : Kurve -> Kurves -> Kurves
kurvesAfterCheckingThisKurve checkedKurve =
Expand All @@ -232,46 +236,65 @@ checkIndividualKurve config tick kurve ( checkedKurvesGenerator, occupiedPixels,
modifyAlive ((::) checkedKurve)
in
( Random.map2 kurvesAfterCheckingThisKurve checkedKurveGenerator checkedKurvesGenerator
, occupiedPixelsAfterCheckingThisKurve
, coloredDrawingPositionsAfterCheckingThisKurve
, occupiedPixelPositionsAfterCheckingThisKurve
, coloredPixelPositionsAfterCheckingThisKurve
)


evaluateMove : Config -> Position -> Position -> Set Pixel -> Kurve.HoleStatus -> ( List DrawingPosition, Kurve.Fate )
evaluateMove config startingPoint desiredEndPoint occupiedPixels holeStatus =
evaluateMove : Config -> Position -> Position -> Set Pixel -> Kurve.HoleStatus -> ( List Pixel, Kurve.Fate )
evaluateMove config startingPoint desiredEndPoint occupiedPixelPositions holeStatus =
let
startingPointAsDrawingPosition : DrawingPosition
startingPointAsDrawingPosition =
World.drawingPosition startingPoint
startingPointAsPixel : Pixel
startingPointAsPixel =
toPixel startingPoint

positionsToCheck : List DrawingPosition
positionsToCheck =
World.desiredDrawingPositions startingPoint desiredEndPoint
pixelPositionsToCheck : List Pixel
pixelPositionsToCheck =
World.desiredPixelPositions startingPoint desiredEndPoint

checkPositions : List DrawingPosition -> DrawingPosition -> List DrawingPosition -> ( List DrawingPosition, Kurve.Fate )
checkPositions : List Pixel -> Pixel -> List Pixel -> ( List Pixel, Kurve.Fate )
checkPositions checked lastChecked remaining =
case remaining of
[] ->
( checked, Kurve.Lives )

current :: rest ->
let
theHitbox : Set Pixel
theHitbox =
World.hitbox lastChecked current
( currentX, currentY ) =
current

crashesIntoWall : Bool
crashesIntoWall =
let
halfThicknessRoundedDown : Int
halfThicknessRoundedDown =
-- TODO: explain
theThickness // 2
in
List.member True
[ current.leftEdge < 0
, current.topEdge < 0
, current.leftEdge > config.world.width - theThickness
, current.topEdge > config.world.height - theThickness
[ currentX < halfThicknessRoundedDown
, currentY < halfThicknessRoundedDown
, currentX > config.world.width - halfThicknessRoundedDown - 1
, currentY > config.world.height - halfThicknessRoundedDown - 1
]

nearbyPositionsX : List Int
nearbyPositionsX =
List.range (currentX - theThickness) (currentX + theThickness)

nearbyPositionsY : List Int
nearbyPositionsY =
List.range (currentY - theThickness) (currentY + theThickness)

nearbyOccupiedPixelPositions : List Pixel
nearbyOccupiedPixelPositions =
nearbyPositionsX
|> List.concatMap (\x -> nearbyPositionsY |> List.map (\y -> ( x, y )))
|> List.filter (\pixel -> Set.member pixel occupiedPixelPositions)

crashesIntoKurve : Bool
crashesIntoKurve =
not <| Set.isEmpty <| Set.intersect theHitbox occupiedPixels
nearbyOccupiedPixelPositions |> List.any (checkCollision current lastChecked)

dies : Bool
dies =
Expand All @@ -293,10 +316,10 @@ evaluateMove config startingPoint desiredEndPoint occupiedPixels holeStatus =
False

( checkedPositionsReversed, evaluatedStatus ) =
checkPositions [] startingPointAsDrawingPosition positionsToCheck
checkPositions [] startingPointAsPixel pixelPositionsToCheck

positionsToDraw : List DrawingPosition
positionsToDraw =
pixelPositionsToDraw : List Pixel
pixelPositionsToDraw =
if isHoly then
case evaluatedStatus of
Kurve.Lives ->
Expand All @@ -306,16 +329,43 @@ evaluateMove config startingPoint desiredEndPoint occupiedPixels holeStatus =
-- The Kurve's head must always be drawn when they die, even if they are in the middle of a hole.
-- If the Kurve couldn't draw at all in this tick, then the last position where the Kurve could draw before dying (and therefore the one to draw to represent the Kurve's death) is this tick's starting point.
-- Otherwise, the last position where the Kurve could draw is the last checked position before death occurred.
List.singleton <| Maybe.withDefault startingPointAsDrawingPosition <| List.head checkedPositionsReversed
List.singleton <| Maybe.withDefault startingPointAsPixel <| List.head checkedPositionsReversed

else
checkedPositionsReversed
in
( positionsToDraw |> List.reverse, evaluatedStatus )
( pixelPositionsToDraw |> List.reverse, evaluatedStatus )


checkCollision : Pixel -> Pixel -> Pixel -> Bool
checkCollision current lastChecked obstacle =
let
is45DegreeDraw : Bool
is45DegreeDraw =
Tuple.first current /= Tuple.first lastChecked && Tuple.second current /= Tuple.second lastChecked

minimumDistanceToObstacle : Float
minimumDistanceToObstacle =
if is45DegreeDraw then
minimumDistanceFor45DegreeDraws

else
theThickness

isTooCloseToObstacle : Bool
isTooCloseToObstacle =
Distance.toFloat (distanceBetween current obstacle) < minimumDistanceToObstacle

isMovingTowardObstacle : Bool
isMovingTowardObstacle =
-- Otherwise every Kurve immediately crashes into its own "neck".
Distance.toFloat (distanceBetween current obstacle) < Distance.toFloat (distanceBetween lastChecked obstacle)
in
isTooCloseToObstacle && isMovingTowardObstacle


updateKurve : Config -> TurningState -> Set Pixel -> Kurve -> ( List DrawingPosition, Random.Generator Kurve, Kurve.Fate )
updateKurve config turningState occupiedPixels kurve =
updateKurve : Config -> TurningState -> Set Pixel -> Kurve -> ( List Pixel, Random.Generator Kurve, Kurve.Fate )
updateKurve config turningState occupiedPixelPositions kurve =
let
distanceTraveledSinceLastTick : Float
distanceTraveledSinceLastTick =
Expand All @@ -336,12 +386,12 @@ updateKurve config turningState occupiedPixels kurve =
y - distanceTraveledSinceLastTick * Angle.sin newDirection
)

( confirmedDrawingPositions, fate ) =
( confirmedPixelPositions, fate ) =
evaluateMove
config
kurve.state.position
newPosition
occupiedPixels
occupiedPixelPositions
kurve.state.holeStatus

newHoleStatusGenerator : Random.Generator Kurve.HoleStatus
Expand All @@ -363,7 +413,7 @@ updateKurve config turningState occupiedPixels kurve =
newKurve =
newKurveState |> Random.map (\s -> { kurve | state = s })
in
( confirmedDrawingPositions
( confirmedPixelPositions
, newKurve
, fate
)
Expand Down
2 changes: 1 addition & 1 deletion src/Round.elm
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import World exposing (Pixel)

type alias Round =
{ kurves : Kurves
, occupiedPixels : Set Pixel
, occupiedPixelPositions : Set Pixel
, initialState : RoundInitialState
, seed : Random.Seed
}
Expand Down
2 changes: 1 addition & 1 deletion src/Spawn.elm
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ isTooCloseFor numberOfPlayers config point1 point2 =
maxAllowedMinimumDistance =
2 * sqrt (config.spawn.protectionAudacity * availableArea / (toFloat numberOfPlayers * pi))
in
Distance.toFloat (distanceBetween point1 point2) < min desiredMinimumDistance maxAllowedMinimumDistance
Distance.toFloat (distanceBetween (World.toPixel point1) (World.toPixel point2)) < min desiredMinimumDistance maxAllowedMinimumDistance


generateKurve : Config -> PlayerId -> Int -> List Position -> Player -> Random.Generator Kurve
Expand Down
7 changes: 6 additions & 1 deletion src/Thickness.elm
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
module Thickness exposing (theThickness)
module Thickness exposing (minimumDistanceFor45DegreeDraws, theThickness)


theThickness : number
theThickness =
3


minimumDistanceFor45DegreeDraws : Float
minimumDistanceFor45DegreeDraws =
sqrt 5
Loading
Loading