Skip to content

Commit

Permalink
Merge pull request #34 from w0rm/spotted-balls
Browse files Browse the repository at this point in the history
Spotted balls
  • Loading branch information
w0rm authored Jan 5, 2024
2 parents e60b896 + 273137a commit a7e854c
Show file tree
Hide file tree
Showing 5 changed files with 430 additions and 54 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The game format is two (2) player 8-Ball. It is a simplified version of [WPA 8-B
5. On scratch, require the next player to place the cue ball anywhere on the table (ball-in-hand) ✅
6. If a player does not pocket one of their balls or scratches, the current player switches ✅
7. Ensure balls are pocketed only once or send an error
8. Support "spotted" balls when the numbered balls fall off the table
8. Support spotting 8 ball when it falls off the table during break ✅

- Winning the game

Expand Down
137 changes: 135 additions & 2 deletions src/Ball.elm
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
module Ball exposing (body, entity, rack, radius)
module Ball exposing (body, entity, rack, radius, spot)

import Angle
import Axis2d exposing (Axis2d)
import Axis3d
import Bodies exposing (Id(..))
import Circle2d exposing (Circle2d)
import Color exposing (Color)
import EightBall
import Direction2d
import Direction3d
import EightBall exposing (Ball)
import Length exposing (Length, Meters)
import Mass exposing (Mass)
import Physics.Body as Body exposing (Body)
import Physics.Coordinates exposing (BodyCoordinates, WorldCoordinates)
import Physics.Material as Material exposing (Material)
import Physics.World as World exposing (World)
import Point2d exposing (Point2d)
import Point3d
import Quantity
import Scene3d exposing (Entity)
import Scene3d.Material as Material
import SketchPlane3d
import Sphere3d
import Vector2d
import Vector3d


Expand Down Expand Up @@ -113,3 +119,130 @@ rack footSpot =
(\( maybeId, pos ) ->
Maybe.map (body >> Body.moveTo pos) maybeId
)


{-| Place a spotted ball on the line behind the foot spot,
such that it doesn't collide with existing balls
-}
spot : Point2d Meters WorldCoordinates -> Ball -> World Id -> World Id
spot footSpot spottedBall world =
let
-- the line behind the foot spot
axis =
Axis2d.through footSpot Direction2d.x

-- the distance from the center of the table to the foot spot
-- is equal to the distance from the foot spot to the foot rail
distanceToFootRail =
Point2d.xCoordinate footSpot

occupiedRange ballPosition =
ballPosition
|> Point3d.projectInto SketchPlane3d.xy
|> Circle2d.withRadius (Quantity.twice radius)
|> intersectBy axis

-- list of occupied ranges on the line behind the foot spot,
-- the endpoints are sorted along the axis, e.g. for balls a, b and c:
-- a1 (f) a2 b1 b2 c1 c2 |
-- foot spot -----> foot rail
occupiedRanges =
world
|> World.bodies
|> List.filterMap
(\b ->
case Body.data b of
Numbered _ ->
occupiedRange (Body.originPoint b)

CueBall ->
occupiedRange (Body.originPoint b)

_ ->
Nothing
)

behindFootSpot =
occupiedRanges
-- collect the furthest endpoints
|> List.map Tuple.second
|> List.filter
(\point ->
Quantity.greaterThan Quantity.zero point
&& Quantity.lessThan distanceToFootRail point
)
-- sort based on the distance to the foot spot
|> List.sortBy Quantity.unwrap

inFrontOfFootSpot =
occupiedRanges
-- collect the nearest endpoints
|> List.map Tuple.first
|> List.filter (Quantity.lessThan Quantity.zero)
-- sort based on the distance to the foot spot
|> List.sortBy (Quantity.unwrap >> negate)

spawnLocation =
(Quantity.zero :: behindFootSpot ++ inFrontOfFootSpot)
|> List.filter
(\distance ->
List.all
(\( start, end ) ->
Quantity.lessThanOrEqualTo start distance
|| Quantity.greaterThanOrEqualTo end distance
)
occupiedRanges
)
|> List.head
-- should never happen, would result in overlapping balls!
|> Maybe.withDefault Quantity.zero
|> Point2d.along axis
|> Point3d.on SketchPlane3d.xy
|> Point3d.translateIn Direction3d.z radius
in
world
|> World.add (body (Numbered spottedBall) |> Body.moveTo spawnLocation)


intersectBy : Axis2d Meters coordinates -> Circle2d Meters coordinates -> Maybe ( Length, Length )
intersectBy axis circle =
let
axisOrigin =
Axis2d.originPoint axis

axisDirection =
Axis2d.direction axis

centerPoint =
Circle2d.centerPoint circle

circleCenterToOrigin =
Vector2d.from centerPoint axisOrigin

cto =
Vector2d.toMeters circleCenterToOrigin

ctoLengthSquared =
cto.x ^ 2 + cto.y ^ 2

dotProduct =
Vector2d.componentIn axisDirection circleCenterToOrigin |> Length.inMeters

r =
Circle2d.radius circle |> Length.inMeters

inRoot =
dotProduct ^ 2 - ctoLengthSquared + r ^ 2
in
if inRoot < 0 then
Nothing

else
let
d1 =
(-dotProduct - sqrt inRoot) |> Length.meters

d2 =
(-dotProduct + sqrt inRoot) |> Length.meters
in
Just ( d1, d2 )
Loading

0 comments on commit a7e854c

Please sign in to comment.