I'm building a small spaceship simulation app that looks a lot like a game for an upcoming talk I'm giving where I use this sample app to teach the F# programming language.

This small app is something like FTL meets Oxygen Not Included where you have a top-down 2D grid of tiles (similar to an old RPG) where each tile has its own mixture of gasses - right now oxygen and carbon dioxide, but potentially others.

I've got a few things I'm trying to simulate:

  1. When new gasses are added to a tile by something like a vent or a life support system, that gas should expand to neighboring tiles if possible
  2. When a pressure changes (e.g. opening a door to another area of the ship or a hull breach), air should flow from the high pressure tile to the low pressure tile next to it.

Given this, and given that some gasses naturally sift to the top of others, I'm trying to figure out a small set of simple rules to govern this behavior.

Previously I had all gasses equalizing with their neighbors and no concept of pressure, but that made it very difficult to treat scenarios like hull ruptures, so I'm looking for something a bit more realistic without getting complex or hyper-accurate.

For example, given tile A with 15g oxygen and 6g CO2 and neighboring tile B of 3g oxygen and 1g CO2, some air should clearly flow from A to B. However, what flows? Is it the lightest gasses? The heaviest gasses? A random or representative sampling of gasses in A? Are there any relevant physics principles I should be aware of?

Note: I posted here instead of in physics because I don't care extremely about nuanced accuracy, just something simple and believable

  • 2
    \$\begingroup\$ Equalizing with neighbours sounds like a simple enough rule, and is effectively what happens in a real hull rupture — it just takes an infinite amount of gas to try to equalize with space so you effectively get a vacuum in short order. What went wrong when you tried it this way? The more you can tell us about specific undesired outcomes, the better we can target solutions to avoid those outcomes, or produce specific desired outcomes you describe. \$\endgroup\$ – DMGregory Jun 13 '20 at 10:51
  • 1
    \$\begingroup\$ Thank you. I worked through it again and shared some obstacles and my solutions as an answer below. \$\endgroup\$ – Matt Eland Jun 13 '20 at 20:53

Following on DMGregory's comment, and some late night / early morning ideas, I tried something a bit different today.

Previously I'd tried:

  1. Every "turn" allow X percentage of a tile's gas to move, split evenly between neighboring tiles who had less of that gas. This worked, but resulted in slow movement.
  2. Removing that percentage limit and instead allowing as much gas to move, but still split evenly between potential neighbors that could take it. This improved things a little, but was still very low in terms of hull ruptures.
  3. Shifting to an equalization model where pressure was the only thing that mattered. If a tile had more pressure than its neighbors, it would shift its topmost gasses into its neighbor with the least pressure until pressure was equalized. It would then repeat this until pressure was equalized. This was fast, but it resulted in pockets of CO2 that never moved around and Oxygen that never made it into tiles
  4. In addition to this pressurization model, light heavy gasses would attempt to sink by flowing into their neighboring tiles. This had about the same problems as the prior approach did, and additional complexity.

The working approach that I found is this:

  1. Loop through each tile
  2. Loop through each gas in the tile
  3. While a tile has more of that gas than its neighbor with the least amount of that gas, flow that gas into that tile. This is done recursively so that gas flows into all neighbors, equalizing pressure of that gas.

This results in each gas spreading throughout the room and pressure invariably equaling out throughout the room. It also results in constant small currents of gasses flowing around, which is fine.

Additionally, when a tile is exposed to space, all of its gasses flow into it, and all of its neighbors start flowing gasses into that lower pressure area. This results in gasses flowing out of the tiles. It's not an extremely fast process for larger rooms, but it works better than my old process.

In terms of code, this looks like the following in F# (read from the bottommost function up):

  let private tryFindTargetForGasSpread gas pos world =
    let tile = world |> getTile pos
    let currentGas = getTileGas gas tile
    getContext(world, tile)
    |> getPresentNeighbors
    |> List.filter(fun n -> canGasFlowInto n && getTileGas gas n < currentGas)
    |> List.sortBy(fun n -> getTileGas gas n)
    |> List.tryHead

  let rec private equalizeTileGas pos gas world =
    let tile = world |> getTile pos
    let target = tryFindTargetForGasSpread gas pos world
    match target with
    | None -> world
    | Some neighbor ->
      |> shiftGas tile neighbor gas
      |> equalizeTileGas tile.Pos gas // May be more gas to shift

  let simulateTileGas pos world = spreadableGasses |> List.fold(fun newWorld gas -> newWorld |> equalizeTileGas pos gas) world

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.