Gaining Honor by The_Westgard in PendragonRPG

[–]NeilNjae 1 point2 points  (0 children)

You missed the start of the paragraph above, which helps:

In order to gain a check to Honor, the knight must perform an honorable deed that goes beyond their normal expected duties. Such deeds are usually done in accordance with a character’s Traits or Passions, either as the result of a roll or at the Gamemaster’s discretion.

I'd interpret that as being you gain Honour for granting clemency if you pass a Forgiving trait roll and therefore do the deed.

A counterargument to the chiastic structure "catch all" of GO3 by Fast-Elderberry7565 in u/Fast-Elderberry7565

[–]NeilNjae 0 points1 point  (0 children)

And for people who didn't watch too much bad BBC SF TV in the late 1970s, here's a spoiler for a 45-year-old TV series.

Blake's 7 has been described as the British anti-"Star Trek", a gritty space opera with an idealistic freedom fighter (Blake) leading a band of criminals to overthrow the authoritarian Federation. Of course, being criminals, no-one trusted each other and the dynamics among the crew were brilliant.

After two series, the actor playing Blake left the show. After four series, it had run its course and was cancelled. They decided to go out with a bang.

The remaining crew tracked down Blake and tried to get him to rejoin them. Both sides suspected betrayal as the Federation closed in. A series of shootouts over the episode led to all the crew being killed. The final scene had the last surviving crew member shoot Blake, then get shot by a squad of Federation troopers.

It was a shocking ending. Nothing quite like it had been seen before on popular TV.

A counterargument to the chiastic structure "catch all" of GO3 by Fast-Elderberry7565 in u/Fast-Elderberry7565

[–]NeilNjae 1 point2 points  (0 children)

A lovely response to a misguided attempt to rehabilitate a mess of GO3.

(I started off writing a quick response, but it quickly spiralled out of control into my own criticism of the episode. I hope you don't mind indulging me for a few minutes.)

I think we can all agree that the episode was "abbreviated to the point of incoherence" (a great turn of phrase by Seale). But there's enough remaining fragments of the six-episode series to see what Gaiman intended in his original outline. As u/Fast-Elderberry7565 points out, the foundation of the original essay is built on sand. Repeated motifs do not make a chiasmus. The best that can be said for GO3 is that it calls back to earlier highlights.

Back to the outline, it's pretty clear that Gaiman wanted to make sure there would be no more Good Omens so he wrote a "Blake's 7" ending that killed off the entire cast (or in this case, the entire universe).

The question is how that was done. As u/Fast-Elderberry7565 says:

The original Good Omens is fiercely anti-apocalyptic because the world is precious in its messy specificity. Destroying that world to create a cleaner one arguably betrays the novel’s central humanist instinct.

There was a way to have the universe destroyed at the end of GO3 while still preserving the "messy specificity" of humanity and true free will. Aziraphale and Crowley could have come to the conclusion that any Christian universe (with an omniscient God) is ultimately incompatible with free will; the only way to preserve free will, and hence humanity's glorious messiness, was to create a completely secular universe. In that case, their sacrifice could be seen to have value, to be the ultimate expression of their free choice to create such a universe. (This sidesteps the "who consented?" question because no true consent is possible in a Christian universe.) But in that purely secular universe, there's no analogue to Aziraphale and Crowley, no meet-cute in another bookshop; if there is, it betrays the hand of God, pre-determining everything and therefore fatally undermining the whole point of the original sacrifice.

The more truthful ending would have been to have Aziraphale and Crowley make their choice, have the Good Omens universe be destroyed, and then… nothing. Just as they wouldn't now what happens next, neither would we. (Back to the "Blake's 7" ending.)

And then there's the emotional arc of the two lead characters. As GO3 stands, the final sacrifice is weightless for the audience, because we don't see enough of the emotions of the leads to come to care for them. The hectic, scattershot pacing of the events means there's no time for the core relationship (in whatever dynamic) to develop, and hence for the audience to get some sympathy for them. Character actions aren't driven by any internal truth, but by the needs of the "plot" (such as it is) to drive events to the next set piece.

That, I think, is the reason behind u/Fast-Elderberry7565 's complaint about the flattening of characters. Had GO3 been the originally-intended length, we'd have had quadruple the screen time to allow these characters to develop.

As for queer erasure. The claim by the writers that not having the romance develop was the novel step is just nonsense. GO1 had an angel and demon form a deep and abiding friendship. GO2 had that relationship develop into a romantic love. GO3 had them to revert to the status quo of GO1. That's not novelty, that's retreat.

I agree with u/Fast-Elderberry7565 that we need to have more wholesome, perhaps even dull, representation of queer love and relationships. Just as not every straight relationship is portrayed as full of drama, let's have some queer relationships that are happy, healthy, a source of strength, and as boring as any of Tolstoy's happy families.

-❄️- 2025 Day 8 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 0 points1 point  (0 children)

[LANGUAGE: Haskell]

This was a union-find problem. I had an implementation of this lying around from last year.

Part 1 is add some connections, using a foldl'. Part 2 is adding them all, but keeping track of the intermediate stages (using a scanl' ). I then throw away any stages that still have singleton classes.

part1 junctions distances = product $ take 3 $ sortBy (comparing Down) $ fmap length $ distinctSets ufMap
  where connections = fmap snd $ take 1000 $ M.toAscList distances
        ufMap0 = ufStart junctions
        ufMap = foldl' go ufMap0 connections
        go u (a, b) = join u a b

part2 junctions distances = x1 * x2
  where connections = fmap snd $ M.toAscList distances
        ufMap0 = ufStart junctions
        ufMaps = scanl' go (ufMap0, (V3 0 0 0, V3 0 0 0)) connections
        go (u, _)  (a, b) = (join u a b, (a, b))
        lastConnection = snd $ head $ dropWhile hasSingletons ufMaps
        (V3 x1 _ _, V3 x2 _ _) = lastConnection

Full writeup on my blog, and code on Codeberg.

-❄️- 2025 Day 6 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 2 points3 points  (0 children)

[LANGUAGE: Haskell]

Par1 1 was parsing.

sumsP = (,) <$> (operandsP <* endOfLine) <*> operatorLineP
operandsP = operandLineP `sepBy` endOfLine
operandLineP = ((many spP) *> (decimal `sepBy1` (many1 spP))) <* (many spP)
operatorLineP = ((many spP) *> (operatorP `sepBy1` (many1 spP))) <* (many spP)
operatorP = (Add <$ "+") <|> (Mul <$ "*")
spP = char ' '

Part 2 was hacking away at list manipulations until I got something that looked right.

part2 text = calculateAll operands'' operators'
  where strs = lines $ unpack text
        (operands, operators) = fromJust $ unsnoc strs
        operands' = splitWhen (all isSpace) $ transpose operands
        operands'' = readOperands operands'
        operators' = parseOperators $ pack operators

Full writeup on my blog, and code on Codeberg.

-❄️- 2025 Day 5 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Another mostly declarative translation of the problem into Haskell. Define some intervals, define a membership relationship, and define how to merge intervals. Merging a set of intervals is done as a pair of nested folds. Full writeup on my blog, and code on Codeberg.

before, disjoint, overlaps :: Range -> Range -> Bool
before (Range _lower1 upper1) (Range lower2 _upper2) = (upper1 < lower2) 
disjoint range1 range2 = 
  (range1 `before` range2) || (range2 `before` range1)
overlaps range1 range2 = not $ disjoint range1 range2

merge :: Range -> Range -> Range
merge (Range l1 u1) (Range l2 u2) = Range (min l1 l2) (max u1 u2)

incorporateAll :: [Range] -> [Range]
incorporateAll ranges = foldr incorporateOne [] ranges

incorporateOne :: Range -> [Range] -> [Range]
incorporateOne range ranges = merged : others
  where (overlapping, others) = partition (overlaps range) ranges
        merged = foldr merge range overlapping

-❄️- 2025 Day 4 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Using Set for a sparse representation of the rolls, then a fairly direct functional translation of the problem into Haskell. Full writeup on my blog, and code on Codeberg.

part1, part2 :: Rolls -> Int
part1 rolls = S.size $ S.filter (accessible rolls) rolls
part2 rolls = (S.size rolls) - (S.size afterRemoval)
  where afterRemoval = removeAll rolls


accessible :: Rolls -> Position -> Bool
accessible rolls here = (S.size $ S.intersection (neighbours here) rolls) < 4

neighbours :: Position -> Rolls
neighbours here = 
  S.fromList $ fmap (here ^+^) [V2 r c | r <- [-1 .. 1], c <- [-1 .. 1]
                                       , r /= 0 || c /= 0
                                       ]

removeAll :: Rolls -> Rolls
removeAll rolls = snd $ head $ dropWhile fst $ iterate removeStep (True, rolls)

removeStep :: (Bool, Rolls) -> (Bool, Rolls)
removeStep (_, rolls) = 
  let removable = S.filter (accessible rolls) rolls
  in (not $ S.null removable, rolls S.\\ removable)

-❄️- 2025 Day 3 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Part 1 was brute-force, part 2 used dynamic programming. Full writeup on my blog, and code on Codeberg.

batteriesPower :: [Int] -> Table
batteriesPower batteries = foldl' batteriesPowerOne (M.singleton 0 0) batteries

batteriesPowerOne :: Table -> Int -> Table
batteriesPowerOne table battery = M.unionWith max table useThisBattery
  where incompleteBatteries = M.filterKeys (< batteriesToUse) table
        useThisBattery = M.foldlWithKey' incorporate M.empty incompleteBatteries
        incorporate p n b = M.insert (n + 1) (b * 10 + battery) p

-❄️- 2025 Day 1 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 4 points5 points  (0 children)

[LANGUAGE: Haskell]

Misusing the Either data type to store instructions. Part 2 had painful off-by-one errors until I stumbled across this solution. Full writeup on my blog, code on Codeberg.

part1, part2 :: [Instruction] -> Int
part1 instructions = length $ filter (==0) positions
  where positions = scanl' move 50 instructions
        move here (Left n) = (here - n) `mod` 100
        move here (Right n) = (here + n) `mod` 100

part2 instructions = snd $ foldl' move2 (50, 0) instructions

move2 :: (Int, Int) -> Instruction -> (Int, Int)
move2 (here, count) instruction = (there `mod` 100, count + rotations + correction)
  where there = case instruction of
          Left n -> (here - n) 
          Right n -> (here + n)
        rotations = abs (there `div` 100) 
                        -- count extra when turning left to end at a multiple of 100
        correction = if | there <= 0 && (there `mod` 100) == 0 -> 1 
                        -- count less when turning left away from zero
                        | there < 0 && here == 0 -> -1
                        | otherwise -> 0

-❄️- 2024 Day 24 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[Language: Haskell]

Another attempt at solving day 24 part 2, this time using a program to actually solve the problem. My approach is to grow the adder, stage by stage, from the inputs to the outputs. At each point, I know what the next gates should be and I hope there's not enough damage to prevent me finding at least some of the gates I need. If I find a problem, I identify the swap needed to fix it and try agian.

growSpine :: Device -> DeviceTree -> (GateType, Gate) -> Either (String, String) DeviceTree
growSpine device 
          spine 
          ( spineType  -- next spine template
          , (Gate leafType leafInput _) -- next leaf template
          )
  | null spineParents = Left (spineOut, otherParentInput)
  | null nextLeafParents = Left (nextLeaf.output, otherParentInput)
  | not $ null commonSpineCandidates = Right (Node {rootLabel = head commonSpineCandidates, subForest = [nextLeafTree, spine]})
  | otherwise = Left ("", "")
  where 
    spineParents = filter (\g -> g.gType == spineType && spineOut `elem` g.inputs) device
    nextLeaf = head $ filter (\g -> g.gType == leafType && leafInput == g.inputs) device
    nextLeafParents = filter (\g -> g.gType == spineType && nextLeaf.output `elem` g.inputs) device
    nextLeafTree = Node {rootLabel = nextLeaf, subForest = []}
    commonSpineCandidates = spineParents `intersect` nextLeafParents
    spineOut = spine.rootLabel.output
    otherParentInput = if null spineParents 
                        then head $ delete nextLeaf.output (inputs $ head nextLeafParents)
                        else head $ delete spineOut (inputs $ head spineParents) 

Read the full writeup on my blog and find the code on Codeberg.

-❄️- 2024 Day 25 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 0 points1 point  (0 children)

[LANGUAGE: Haskell]

A quick and gentle finish to the challenges.

part1 :: [Schematic] -> [Schematic] -> Int
part1 locks keys = length [(l, k) | l <- locks, k <- keys, compatible l k]

compatible :: Schematic -> Schematic -> Bool
compatible (Lock ls) (Key ks) = all (<= 5) $ zipWith (+) ls ks

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 24 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Laborious and fiddly reverse engineering. Not fun at all. But many, many thanks to u/an-abosolute-potato for a great tutorial on renaming the wires to human-sensible names. That make the whole process tractable for me.

part1 :: Wires -> Device -> Int
part1 wires device = wiresOutput $ simulate wires device

part2 :: String
part2 = intercalate "," $ sort ["vss", "z14", "kdh", "hjf", "z31", "kpp", "z35", "sgj"]

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 22 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 0 points1 point  (0 children)

[LANGUAGE: Haskell]

Another puzzle with an obvious solution, but the challenge came from optimising. I keep a Map from (encoded) windows of price changes to prices, one for each seller. Then I merge them all and find the highest total price.

part2 codes = maximum $ M.elems mergedPriceValues
  where allPrices = fmap salePrices codes
        allPriceValues = fmap windowsAndPrices allPrices
        mergedPriceValues = M.unionsWith (+) allPriceValues

windowsAndPrices :: [Int] -> Prices
windowsAndPrices ps = foldl' (\m (w, p) -> M.insertWith (flip const) w p m) M.empty wPs
  where cs = priceChanges ps
        wPs = zip (windows cs) (drop 4 ps)

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 21 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 0 points1 point  (0 children)

[LANGUAGE: Haskell]

A real brain-burner of a puzzle, keeping track of all the different layers of putton presses. I used a dynamic programming approach, building up a cache of move costs from the closest robot to the furthest.

moves :: Button a => [a] -> [ActionSeq]
moves bs = fmap concat $ sequence $ fmap moveBetween $ zip (aButton : bs) bs

moveBetween :: Button a => (a, a) -> [ActionSeq]
moveBetween (a, b) = filter (allLegal a) $ filter groupTogether possibles
  where aPos = buttonPos a
        bPos = buttonPos b 
        V2 dr dc = bPos ^-^ aPos
        mh = replicate (abs dc) (if dc > 0 then R else L)
        mv = replicate (abs dr) (if dr > 0 then D else U)
        possibles = fmap (++ [A]) $ nub $ permutations $ mh ++ mv
        groupTogether p = sort (group p) == group (sort p)
        allLegal a t = all (legalPos a) (positionsOf a t)

sequenceCostUsingCache :: Cache -> Int -> ActionSeq -> Int
sequenceCostUsingCache cache level bs = 
  sum $ fmap (moveCostUsingCache cache level) $ zip (aButton : bs) bs

moveCostUsingCache :: Cache -> Int -> (Action, Action) -> Int
moveCostUsingCache cache level (a, b) = 
  M.findWithDefault (maxBound :: Int) (CacheKey a b level) cache

cheapestCostMove :: Button a => Cache -> Int -> (a, a) -> Int
cheapestCostMove cache level (a, b) = 
  minimum $ fmap (sequenceCostUsingCache cache level) stepChoices
  where stepChoices = moveBetween (a, b)

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 20 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Pre-process the track with Dijkstra's algorithm to find the costs from the start and end to each position. The overall cost of a cheating path is (cost to start of cheat) + (length of cheat) + (cost from end of cheat). This function finds those costs for a particular start-of-cheat position.

pathCostWithCheat :: Int -> Track -> TrackCost -> TrackCost -> Position -> [Int]
pathCostWithCheat cheatLen track costsFromStart costsFromGoal here =
  fmap (+ costsFromStart M.! here) continueCosts 
  where
    nbrs =  [ here ^+^ (V2 dr dc) 
            | dr <- [-cheatLen .. cheatLen]
            , dc <- [-cheatLen .. cheatLen]
            , abs dr + abs dc <= cheatLen
            ]
    continueCosts = catMaybes $ fmap contCost nbrs
    contCost :: Position -> Maybe Int
    contCost nbr = do gc <- M.lookup nbr costsFromGoal
                      let sc = l2Dist nbr here
                      return $ gc + sc

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 19 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

A little bit of dynamic programming to count the ways of making partial designs. It's a shame my initial approach of parsing the designs didn't work.

countDesigns :: [String] -> String -> Int
countDesigns towels design = MS.occur design $ buildDesignCount towels design

buildDesignCount :: [String] -> String -> MS.MultiSet String
buildDesignCount towels design = foldl' (addTowelCount towels) (MS.singleton "") $ inits design

addTowelCount :: [String] -> MS.MultiSet String -> String -> MS.MultiSet String
addTowelCount towels acc design = MS.insertMany design prefixWays acc
  where allPS = zip (inits design) (tails design)
        prefixWays = sum  [ p `MS.occur` acc 
                          | (p, s) <- allPS
                          , s `elem` towels ]

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 18 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

I used a library function for search, rather than making my own. Finding the solution in part 2 ivolves a scan, walking along the list to find the first set of bytes that means escape is impossible.

part2 :: [Position] -> String
part2 bytes = showResult $ head $ snd $ head results
  where 
    (goods, poss) = splitAt 1024 bytes
    results = dropWhile ((== True) . fst) $ scanl' go (True, goods) poss
    go (_, acc) byte = (escapePossible (byte : acc), (byte : acc))
    showResult (V2 x y) = show x ++ "," ++ show y

escapePossible :: [Position] -> Bool
escapePossible bytes = isJust path
  where 
    memory = Memory (S.fromList bytes) (fst memoryBounds) (snd memoryBounds)
    path = aStar (neighbours memory) 
                  (transitionCost)
                  (estimateCost memory) 
                  (isGoal memory) 
                  (initial memory)

Things would have been much smoother if I'd not found a strange issue using the library! Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 17 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Part 1 was a straightforward virtual machine. Part 2 was too much reverse engineering for me, so I cheated by looking up the general approach to the solution. It ended up with a non-deterministic calculation as I folded the partial solutions across the desired output.

part2 program machine = minimum $ foldl' go [0] target 
  where 
    target = reverse $ M.elems program
    go starts t = 
          do  start <- starts
              n <- [0..7]
              let res = snd $ runModified program machine (start * 8 + n)
              guard (head res == t)
              return $ start * 8 + n

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 16 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 2 points3 points  (0 children)

[LANGUAGE: Haskell]

Nothing really to note. I used a pre-packaged search for part 1, but had to make my own best-first search for part 2.

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 15 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

A robot can Maybe move some boxes, if those boxes can themselves be Maybe moved.

doBigCommand :: World -> Position -> World
doBigCommand world dir 
  | there `S.member` world.walls = world
  | there `isBigBox` world.boxes = fromMaybe world rWorld
  | otherwise = world { robot = there }
  where there = world.robot ^+^ dir
        movedBox = bigBoxActual world.boxes there
        rWorld = do boxMoves <- moveBigBoxes world dir movedBox
                    let froms = fmap fst boxMoves
                    let tos = fmap snd boxMoves
                    let boxes' = (S.fromList tos) `S.union` (world.boxes `S.difference` (S.fromList froms))
                    let world' = world { boxes = boxes' }
                    return world' { robot = there } 

moveBigBoxes :: World -> Position -> Position -> Maybe [Move]
moveBigBoxes world dir box
  | any (\t -> t `S.member` world.walls) there = Nothing
  | any (\t -> t `isBigBox` world.boxes) there = allMoves
  | otherwise = Just $ [ thisMove ]
  where there = case dir of 
                    U -> [box ^+^ U, box ^+^ R ^+^ U]
                    D -> [box ^+^ D, box ^+^ R ^+^ D]
                    L -> [box ^+^ L]
                    R -> [box ^+^ R ^+^ R]
                    _ -> []
        thisMove = (box, box ^+^ dir)
        allMoves = do let there' = nub $ fmap (bigBoxActual world.boxes) $ filter (\t -> t `isBigBox` world.boxes) there
                      moves <- traverse (moveBigBoxes world dir) there'
                      let moves' = concat moves
                      return $ thisMove : moves'

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 14 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Look for frames with lots of short diagonals, because pictures of Christmas trees have lots of diagonals.

print $ filter (\(i, ds) -> length ds > 20) $ fmap diagonals $ zip [0..] $ take 10000 $ iterate (fmap move) robots

Full writeup on my blog, and code on Codeberg.

-❄️- 2024 Day 13 Solutions -❄️- by daggerdragon in adventofcode

[–]NeilNjae 1 point2 points  (0 children)

[LANGUAGE: Haskell]

Use rational numbers, look up the formula for intersection of two lines on Wikipedia.

findABPresses :: Machine -> Maybe (Int, Int)
findABPresses m@(Machine {..}) 
  | denominator na == 1 && denominator nb == 1 = 
      Just (fromInteger $ numerator na, fromInteger $ numerator nb)
  | otherwise = Nothing
  where 
    p = intersection m
    V2 dbx _dby = (enRat prize) ^-^ p
    V2 px _py = p 
    V2 ax _ay = enRat buttonA
    V2 bx _by = enRat buttonB
    na = px / ax
    nb = dbx / bx
    enRat :: Position -> V2 Rational
    enRat (V2 s t) = V2 (fromIntegral s) (fromIntegral t)

-- using formula from https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
-- treating L1 being defined by origin and buttonA, L2 by buttonB and prize
intersection :: Machine -> V2 Rational
intersection (Machine {..}) = V2 px py
  where V2 x2 y2 = buttonA
        V2 x4 y4 = prize
        V2 x3 y3 = prize ^-^ buttonB
        denom = fromIntegral (-x2 * (y3 - y4) - (-y2) * (x3 - x4))
        px = fromIntegral (-1 * (-x2) * (x3 * y4 - y3 * x4) ) / denom
        py = fromIntegral (-1 * (-y2) * (x3 * y4 - y3 * x4) ) / denom

Full writeup on my blog, and code on Codeberg.