Phasmophobia and Bézier Curves

I’ve been playing a lot of Phasmophobia recently. It’s a ghost-hunting game where you, and a team of up-to 3 other investigators explore haunted properties looking for evidence in order to determine what type of ghost is present. At the same time you have to try to not get killed at the hands of said ghost.

So what does this game have to do with Bézier curves??

We’ll get there. Trust me.

In the most recent release, the developers added in some riddles and clues to tease something coming in the next major revision. The problem I ran into was that if I had a question about a riddle I couldn’t solve, googling would only bring up spoiler-filled results. I don’t want answers, but little nudges in the correct direction.

I decided to take it upon myself to create a repository of information about the clues so one can decide which information to be exposed to, thereby minimizing risk of unwanted spoilage. I give you, Phasmophobia Rune Hints.

This still has nothing to do with Bézier curves! Get on with it!

Dear reader, I am nearly there. Believe me!

One of the evidences of ghost ghost activity in the game is what they call “Ghost Orbs”. They are floating balls of light that are only visible on video camera. (In reality these are specs of dust reflecting light close to a lens…) In any case, I wanted to spruce up my rune hinting website with some Phasmophobia appropriate ambiance. I wanted to add some ghost orbs.

My first implementation was to take a PNG of a ghost orb and use javascript to move it in a specific direction while fading it in and out at the ends of the animation. This yielded a result and was maybe passable, but it wasn’t great. My brother suggested to use Bézier curves in order to have it follow a more natural curved path.

In this example, the ghost orbs both start and end at the same location, however the one on the left follows a linear path while the one on the right follows a Bézier curve. (The points are chosen at random, so you may have to watch a few cycles to get an interesting comparison). Below is a simplified example of the code to demonstrate the minimal logic required to compute the curved path.

function find_point_on_line(p1, p2, percent){
    let p3 = {
        "x" : ((p2.x - p1.x) * percent) + p1.x,
        "y" : ((p2.y - p1.y) * percent) + p1.y,
    }
    return p3;
}

function follow_bezier_curve(b1, b2, b3, percent){
    if (percent >= 1) return; // We finished
    let p1 = find_point_on_line(b1, b2, percent);
    let p2 = find_point_on_line(b2, b3, percent);
    let p3 = find_point_on_line(p1, p2, percent);
    move_orb(p3);
    timer = setTimeout(() => follow_bezier_curve(percent+.001), 1000/60);
}

Bézier curves are pretty common. I’ve been using them for decades in software like Adobe Illustrator or Affinity Designer. I remember learning about them in college. But I’ve actually never had a need to program my own.

As it turns out, they are really simple. A quadratic Bézier curve uses 3 points; a, b, and c. First you draw a path from a to b, and from b to c. Then you draw paths between points at equivalent subdivisions on these paths. The result is a really nice curve. It’s really hard to explain with words, so try out this interactive animation instead:

This is the type of Bézier curve I used on the Phasmophobia site.

There are also cubic Bézier curves. Cubic curves are basically two quadratic curves combined. Let’s say you have 4 paints; a, b, c, and d. You would create two quadratic curves; abc, bcd, then your final curve will be created by the same process along the resulting curves.

These 4-point quadratic curves are how vector drawing programs like Illustrator build paths. the outer two points act as nodes, and the inner two act as the handles.

Although not very complicated, I thought it was a fun diversion, and wanted to share.

Wordle; the Latest Craze

Wordle has gotten popular. A cross between hangman and MasterMind, it’s cute, simple, and competitive. It has all the elements required to go viral. I finally decided to indulge in the clever word game about a week before it was purchased by the New York Times.

Since the New York Times took charge, there have only been minor changes. Some people are claiming that the New York Times has changed the word list, and is favoring more difficult words. There have also been some mishaps where users have gotten different words from each other.

The way Wordle was written, it has a list of 2,315 words completely exposed in the JavaScript code. Each day the next word in the list becomes the word of the day. One of the first words I encountered when playing was “moist,” the 228th word in the list. The next word, “shard,” appeared on the subsequent day, and so on.

I decided I wanted to see how the New York Times had modified the list. First, I had to find the original list. The original site, powerlanguage.co.uk/wordle/, now redirects to the New York Times, so in order to get the unadulterated code, I used the WayBack Machine to look at the site as it appeared before the sale.

The array of words is contained in a JavaScript file. I extracted the array of words from both the old and new, then compared.

6 words have been removed:

  • agora
  • pupal
  • lynch
  • fibre
  • slave
  • wench

Both “agora” and “pupal” are words that should have appeared in the last few weeks. In fact, “pupal” is the word some people got when visiting the original Wordle site on February 19, 2022, whereas the users who visited the new New York Times version got the word “swill” (which is two words later in the list).

Excerpt from original Wordle list

"moist","shard","pleat","aloft","skill","elder","frame","humor",
"pause","ulcer","ultra","robin","cynic","agora","aroma","caulk",
"shake","pupal","dodge","swill","tacit","other","thorn"

Excerpt from New York Times list

"moist","shard","pleat","aloft","skill","elder","frame","humor",
"pause","ulcer","ultra","robin","cynic","aroma","caulk","shake",
"dodge","swill","tacit","other","thorn"

Since two omitted words have been encountered, the original and new games are now off by two. The next omitted word, “lynch,” won’t be encountered for another 50 or so days (at the time of writing).

Despite the rumors, no words have been added, and the ordering of words has not been otherwise altered.

Since we are on the subject of Wordle, I decided it would be fun to make a utility to help guess the words. It would be too unsporting to just use the word list that the game provides, so I found a list of the most popular English words, and extracted all the five-letter words. This yielded about 3000 words, which is more than the official Wordle list.

I made a simplistic web app that can be used to input the letters and indicate if they appear green or yellow, or not used at all. The app will return a list of possible words for the given configuration.

I don’t condone cheating, and I don’t use this when I play Wordle. It was merely a fun exercise. You can try it out for yourself here.

Project Euler 43

I like to go back and re-solve Project Euler problems in different languages. Lately, I’ve been solving them in Javascript for fun. When I do this, I don’t look at previous solutions and try to do it from scratch. When I was finished, I was surprised by the performance of my solution to 43 compared to my previous attempts in other languages.

Problem 43 is as follows:

The number, 1406357289, is a 0 to 9 pandigital number because it is made up of each of the digits 0 to 9 in some order, but it also has a rather interesting sub-string divisibility property.

Let d1 be the 1st digit, d2 be the 2nd digit, and so on. In this way, we note the following:

  • d2d3d4=406 is divisible by 2
  • d3d4d5=063 is divisible by 3
  • d4d5d6=635 is divisible by 5
  • d5d6d7=357 is divisible by 7
  • d6d7d8=572 is divisible by 11
  • d7d8d9=728 is divisible by 13
  • d8d9d10=289 is divisible by 17

Find the sum of all 0 to 9 pandigital numbers with this property.

When I first solved this problem, I solved it in C. This was in 2014, and I was still fairly green. My solution at the time was to iterate through every 10-digit number and see if it was pandigital and then if it was, check if it met the sub-divisibility requirement.

This solution is what you would call “brute-force”. It’s inelegant, and slow. However, it does work. It took 33.948 seconds to compute.

A few years later I was doing more with Rust and Python. Both of these solutions I created used the same method. This probably happened because I wrote both solutions close together. At any case, this time I thought myself more clever and took a pandigital number, 1234567890, and discovered every permutation, and then checked for the sub-divisibility requirement of each.

This is better than brute force, but still time consuming. Python can accomplish this in 18.724 seconds and Rust in 4.621. Better, but still not great.

The general rule of thumb with Project Euler is that if a solution takes more than a second, you haven’t found the intended method of solving it.

Looking at it this time around, it seemed like a very straightforward problem with an obvious path for a solution. Instead of finding pandigital numbers and checking if they meet the sub-string divisibility requirement, this time I would build up the pandigital numbers using the sub-strings.

First I created arrays for the multiples of the first 7 primes with 2 and 3 digits. I then used a recursive function to build up a number using valid combinations of these sub-strings (since each one overlaps the next with 2 digits). This creates a much smaller group of numbers to check.

Once I have all my potential pandigital numbers, I check to make sure they are in fact pandigital. (Note that at this stage, they should be missing the first digit). When checking for pandigitality, I’m actually looking for 9 different digits, and if so, I prepend the missing 10th digit and voila, it’s a valid pandigital number!

This solution is much, much faster at .237 seconds.

I’m very pleased with that result, but a little shocked I didn’t see this method when I have solved it previously. It’s nice to know that since I first started solving these problems years ago, I can see measurable improvement in my ability to find and create solutions to these fun little puzzles.

Source on GitHub

© 2007-2015 Michael Caldwell