Drawing ASCII Art to Test a Physics System

//      ____    __
//     / __ \  / /_    __  __  _____  (_) _____  _____
//    / /_/ / / __ \  / / / / / ___/ / / / ___/ / ___/
//   / ____/ / / / / / /_/ / (__  ) / / / /__  (__  )
//  /_/     /_/ /_/  \__, / /____/ /_/  \___/ /____/
//                   /____/

const world = Physics.create.world();
...
world.integrate(1);

expect(draw()).toMatchInlineSnapshot(`
  Array [
    "                   .                ",
    "             .  .  .  .  .          ",
    "          .  .  .  .  .  .  .       ",
    "          .  .  .  .  .  .  .       ",
    "       .  .  .  .  .  .  .  .  .    ",
    "          .  .  .  .  .  .  .       ",
    "          .  .  .  .  .  .  .       ",
    "             .  .  .  .  .          ",
    "                   .           ↖    ",
  ]
`);

Generating my own ASCII art in programming projects is a great way to solve certain hard problems. Diagrams made from PNGs with some kind of rendered documentation is great, but it has a high barrier to entry. Plus, this kind of documentation does not live with the code, which makes it easy to miss and forget about. I have started a series of posts detailing various strategies on how I draw with ASCII.

Testing a Physics System

Physics systems are lot of fun to play around with. However, they end up being fairly error prone, really finnicky with various inputs, and hard to reproduce issues. I was working on my sphere physics system, and noticed that I was getting lots of different errors with points shooting through spheres, but no way to debug the system. I had thousands of individual points, and the simulation was running at 60 frames per second. The issue happened every couple of seconds where a point would zoom across the screen. Now, how to catch that?

I had some suspicions on what was going on, but didn’t have a great way to reproduce them. The numbers that make up a physics system are so abstract as to be almost worthless. I wanted to build something visual. However, setting up an image testing scheme is finnicky at best, and slight differences in renders can easily lead to false negatives.

ASCII to the rescue

Text-only output is great to test against. It can be checked right into the codebase. It can be commented on and included exactly where you are using it. First off, for the physics system. We need to draw points in motion. In order to do this, we need a velocity vector, and then a bit of math to convert that into a direction.

function getArrow(vec: Vec2): string {
  // The directions:
  // 5  6  7
  // 4  ↘  0
  // 3  2  1
  const tau = Math.PI * 2;
  const theta = (tau + Math.atan2(vec.y, vec.x)) % tau;
  switch (Math.round((theta / tau) * 8) % 8) {
    case 0:
      return "→";
    case 1:
      return "↘";
    case 2:
      return "↓";
    case 3:
      return "↙";
    case 4:
      return "←";
    case 5:
      return "↖";
    case 6:
      return "↑";
    case 7:
      return "↗";
    default:
      throw new Error("Unable to convert a vector to an arrow.");
  }

It’s not a typical rendering engine, but it will do. When I first started writing these tests, I kept it pretty simple. But, as I added more test cases I found I needed to encapsulate this logic better. Enter the Stage class.

class Stage {
  constructor(size: number) {...}
  drawSphere(character: string, sphere: Physics.Sphere): void {...}
  drawBox(character: string, box: Physics.Box): void {...}
  drawPoint(character: string, point: Physics.Point): void {...}
  output(): string[] {...}
  clear(): void {...}
}

The full source code for the Stage is available, but one example of drawing a sphere will hopefully suffice to give a general impression of the strategy to render to text.

class Sphere {
  // The rendered text, each entry is 1 string.
  data: string[][];

  // The width and height of the stage.
  size: number;

  ... // Omit the rest of the class definition.

  drawSphere(character: string, sphere: Physics.Sphere): void {

    // Create a point that can be reused, mutably.
    const point = Physics.create.point({ x: 0, y: 0 });

    // Go through the i and j coordinates of the stage, which would be like the
    // x and y coordinates.
    for (let i = 0; i < this.size; i++) {
      for (let j = 0; j < this.size; j++) {

        // Transform the coordinate to be inside of the stage's coordinate
        // space, where (0,0) is the middle of the stage. This makes it
        // easy to center the geometry.
        point.body.position.x = i - this.size / 2;
        point.body.position.y = j - this.size / 2;

        // Re-use the physics system, and test to see if the point is inside
        // of the the sphere. This is equivalent to a pixel rasterization
        // step, but instead this is using text.
        if (Physics.intersect.sphere.point(sphere, point)) {
          this.data[j][i] = ` ${character} `;
        }
      }
    }
  }
}

I will reproduce an entire test case below (written using Jest), as I think it shows off the problem and solution pretty well. Feel free to skim the code itself, and look at the text diagrams. I amended the original code with more comments to explain what is going on.

describe("collisions between point and sphere", function() {
  it("intersects coming from the bottom right", function() {
    // This is the text renderer outlined above. The width
    // and height are each 12 characters long.
    const stage = new Stage(12);

    // Begin to set up the test case with the physics system.
    const point = Physics.create.point({ x: 5, y: 5 });
    const sphere = Physics.create.sphere({ x: 0, y: 0 }, 4);

    // Make sure the point is moving up and to the left in this coordinate space.
    point.body.velocity.x = -1;
    point.body.velocity.y = -1;

    // Chain together some draw calls to draw the sphere and point.
    function draw(): string[] {
      stage.clear();
      stage.drawSphere(".", sphere);
      stage.drawPoint(getArrow(point.body.velocity), point);
      return stage.output();
    }

    // Create the world and add the point and sphere.
    const world = Physics.create.world();
    world.ticksPerSecond = 1;
    world.addToAllGroup(point);
    world.addToAllGroup(sphere);

    // This is the first assertion, the arrow is coming in towards the sphere.

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .          ",
        "                   .                ",
        "                                  ↖ ",
      ]
    `);

    // Step the world forward.

    world.integrate(1);

    // The arrow moved, but is going in the same direction. Jest
    // tests will automagically update the inline snapshots,
    // which cuts down on the manual work. However, it's a bit
    // risky as it can obliterate a valid assertion with a
    // regression.

    // I also like to place text assertions in an array of strings
    // as each line tends to indent better.

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .          ",
        "                   .           ↖    ",
        "                                    ",
      ]
    `);

    world.integrate(1);

    // It's about to hit!

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .  ↖       ",
        "                   .                ",
        "                                    ",
      ]
    `);

    world.integrate(1);

    // The point is reflected off of the surface of the sphere.

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .          ",
        "                   .           ↘    ",
        "                                    ",
      ]
    `);

    world.integrate(1);

    // The velocity is conserved, and the point keeps on going.

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .          ",
        "                   .                ",
        "                                  ↘ ",
      ]
    `);
  });

This was the first test I wrote, and it felt like writing something that targeted the 2d canvas. I could set up the situation visually, and step through the results to see the behavior. Finally, let’s find the bug that I mentioned earlier.

  it("handles points that are inside of spheres on their way in", function() {
    ... // Omit the setup this time.

    // The issue it turned out was what happened when the point
    // was partially embedded in a sphere. Did I mention that
    // physics simulations are a bit messy?

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  ↖  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .          ",
        "                   .                ",
        "                                    ",
      ]
    `);

    world.integrate(1);

    // It turns out that I needed to handle this case in the code.
    // This test could be focused to only run, and I could begin
    // work to handle this case. This was much easier outside of
    // the bigger visualization.
    //
    // The behavior before was that the point shot through the sphere
    // with an insane amount of velocity, creating very odd behavior
    // in an otherwise stable simulation.

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .  ↘       ",
        "                   .                ",
        "                                    ",
      ]
    `);

    world.integrate(1);

    // Watch the arrow zoom away.

    expect(draw()).toMatchInlineSnapshot(`
      Array [
        "                                    ",
        "                                    ",
        "                   .                ",
        "             .  .  .  .  .          ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "       .  .  .  .  .  .  .  .  .    ",
        "          .  .  .  .  .  .  .       ",
        "          .  .  .  .  .  .  .       ",
        "             .  .  .  .  .          ",
        "                   .           ↘    ",
        "                                    ",
      ]
    `);
  });

Jest was quite nice in handling these type of tests, and provided a fairly easy to read diff. I could also re-generate the visuals directly in the code itself using the inline snapshot feature. This next diff shows an example of what a test with an error could look like.

- Snapshot  - 2
+ Received  + 2

  Array [
+   "  ↖                                 ",
-   "                                    ",
    "                                    ",
    "                   .                ",
    "             .  .  .  .  .          ",
    "          .  .  .  .  .  .  .       ",
    "          .  .  .  .  .  .  .       ",
    "       .  .  .  .  .  .  .  .  .    ",
    "          .  .  .  .  .  .  .       ",
    "          .  .  .  .  .  .  .       ",
+   "             .  .  .  .  .          ",
-   "             .  .  .  .  .  ↘       ",
    "                   .                ",
    "                                    ",
  ]

This was a personal visualization project, so I was enjoying exploring the tests and physics system as much as I wanted to see the outcome. Rendering to text made it easy to reproduce a visual system that was made up of very abstract, and magical numbers that would normally be hard to write a test for. This ASCII art trick is fun for a physics system, but it works equally well for other visual data structures that are quite common in computer programming. I’ve definitely used it to a pretty nice effect in the Firefox Profiler.

Finally, if you enjoyed this article, then why not take a break and watch my finished physics simulation.

More From writing

More Posts