James Williams
LinkedInMastodonGithub

The Southwest Debacle

I've worked for an airline before...when multiple hurricanes hit Florida in a short span. I was told I was ruining their weddings/vacations/etc. People straight up lied to me when I had all the evidence to prove it and then cussed me out when I had to call out their lies. I think I've earned a small soapbox to rant about the recent events. Here are the things I think contributed to the catastrophic failure at Southwest:

Problem 1: Point to point flying.

A point to point system relies on the system being generally healthy because you have aircraft coming from wherever going to wherever. So instead of weather(WX) at origin and destination, as a passenger I have to think about WX at every other city that flight has visited before me. This makes for a bad customer experience because "why is my flight delayed or CXLD but not this flight that leaves later?" is a thing. Hub/Spoke do suffer greatly when there is bad WX at a major hub, but it's usually contained somewhat and there is a prioritization of which flights will get takeoff slots (usually international, then hub to hub).

Problem 2: Seating.

Being a decently tall dude, seat anxiety made me stay away from Southwest. The boarding group + number helps a bit but the problem for me is the fact that on Southwest, your boarding pass is a not really a guaranteed seat. They don't by policy overbook (as in intentionally sell more seats than are available) but in the case of a cancellation, reduced capacity/positive space employees, you can end up in an oversold situation (incidentally more passengers than seats). There's a nuance of difference but it's hard for the passenger to understand not unlike the difference between non-stop and direct. The point isn't to hate on open seating but it feels less like I have a guaranteed seat and more like I have a claim ticket to redeem for a seat...maybe that's just me.

Problem 3: Lack of capacity.

Back in the day, I could easily travel non-revenue to Europe with only a small worry of having to kill 4-5 hours at ATL or JFK. Capacity was tight pre-COVID and it still is. Lack of extra capacity means it's harder for the system to absorb passengers from CXLD flights.

Problem 4: Lone wolf mentality.

One little known fact is that while it is not encouraged and nigh impossible to do easily, most airlines can book segments on other airlines. This is beyond what airlines they may codeshare with. These interline agreements allow an airline to handle check-in and carriage of the passenger / baggage. Handoff of baggage usually is seamless. For boarding passes, each airline might reprint the BP in their format but that's a minor inconvenience. This is important in irregular operations(IROP) because airlines with a interline agreement have some capacity to book on other carriers that aren't affected by the same issue.

Southwest by policy doesn't interline with anyone and its capacity isn't even viewable/bookable on the major booking systems. I understand the initial allure when the internet was slow to get folks to come to your site and book. People are more savvy, price check easily and...multiple tabs are a thing.

Problem 5: Lack of tools/That cop

I could be wrong but I've never noticed banks of phones in airports for Southwest to handle changes. At DL, those were called DL Direct and it rang through to me at a higher priority than elite members. I even had more power on those calls because I was deemed to be "at the airport." It was airport, reissues, Skymiles/Elite, General Sales IIRC.

These desks are usually PAST SECURITY. The advice to exit the secure area and talk to the check-in agents endangers the other flights leaving that day because you will have folks with a departing flight mixed in with those who are canceled and if the worst customer service to have found someone a seat but have to let it go because there is no way they'll get through security in ATL before boarding or tell someone that you gave their seat away because they were in line and not checked in because someone told 200 people to get in line ahead of them. When I "protected" a passenger on a flight because they were likely to miss their connection, the boarding pass from the previous flight still scan. If the gate agent had extra time, they would print new boarding cards for the folks, otherwise during boarding, the existing boarding card would spit out a seat assignment ticket. IE STILL VALID IANAL so don't get arrested but staying inside the secure area is in your best interest most of the time.

How does Southwest recover?

It seems like they have been living in the mindset of "if that flight is CXLD, there will be another flight in an hour or two." Climate change affects everything. If that 100 year event becomes a 5, 10, or 20 year event and it's not in your modeling, that's a problem.

I hope the company invests in the tech side. They need to start thinking about themselves as a tech company. Given the other things I didn't touch on like the FAA fines if flight crew goes over their legal limit, perhaps this multi-day shutdown of the full system was the only way to reset things. I wouldn't want to explain this to the customer when all the other airlines are fine. If you are a Southwest passenger, be nice to the agent on the phone, they are trying their hardest to help you. Your salty attitude x 80 or 100 calls is what they've been dealing with on a daily basis. Don't get yourself put in the penalty box or accidentally dropped.

Permalink

Advent Of Code 2022 - Day 6 - Tuning Trouble

For day 6, you need to help the elves with their communication system. Their devices receive a series of characters, one at a time. The start of a usable data packet is indicated by some x characters that are unique.

If you are doing this problem in Kotlin, 95% of the work can be done for you using a pre-defined function in the collections library. I, however, didn't think of it until AFTER completing both stars.

Part I and II

I used much of the same logic for both parts as the only difference was the required number of unique characters. I iterated over the string from zero to string length minus the required size. On each iteration, I created a substring for the target size and tested it for uniqueness.

package adventofcode.y2022
import adventofcode.AdventOfCode
import adventofcode.DayOf2022
import java.util.*

class Day06 : DayOf2022(6) {
    var scan: Scanner

    init {
        //DEBUG = true
        scan = if(DEBUG)
            testScanner
        else scanner
    }

    lateinit var part2Line:String
    override fun part01(): Any? {
        val line = scan.nextLine()
        part2Line = line
        for (i in 0..line.length-4) {
            val x = line.substring(i..i+3).toCharArray().distinct()
            if (x.size == 4) {
                return i+4
            }
        }
        return super.part01()
    }

    override fun part02(): Any? {
        val line = part2Line
        for (i in 0..line.length-14) {
            val x = line.substring(i..i+13).toCharArray().distinct()
            if (x.size == 14) {
                return i+14
            }
        }

        return super.part02()
    }
}

fun main() = AdventOfCode.mainify(Day06())
 

If you are thinking that sounds a lot like windowed with the requirement for no partial windows. You are totally correct.

Permalink

Advent Of Code 2022 - Day 5 - Supply Stacks

When I start a new problem, a lot of times I will look at the sample test data first to try and understand what I might need to do with the data. This test data for Day 5 had me perplexed for a bit.

    [D]
[N] [C]
[Z] [M] [P]
 1   2   3

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2
 

The premise is that you are in the supply docks and have a crane moving around containers. You must move them in a prescribed order and report their final state.

Part I

After a while, I realized it was the combination of a stack/queue problem and a regular expression problem. I HATE doing regexes. Further, in the first section of the input, the last line had the ids of the stacks and I could use that index position to find all the crates in that stack. The lines were varying lengths so I needed to make sure to not throw an index error but that was easy to manage.

val stacks = mutableListOf()
while(scan.hasNextLine()) {
    val line = scan.nextLine()
    if (line.isEmpty()) {
        break
    } else {
        stacks.add(line)
    }
}

val lastLine = stacks[stacks.size - 1]
val stackIds = stacks[stacks.size - 1].split(" ").filter{ it.isNotEmpty()}

val indices = stackIds.map { lastLine.indexOf(""+it) }

queues = Array>(stackIds.size) {i -> ArrayDeque()}
for (i in 0..stacks.size-2) {
    stackIds.forEachIndexed { j, value ->
            val index = indices[j]
            if (stacks[i].length > index) {
                val c = stacks[i][index]
                println("value  c:" + c)
            }
            if (stacks[i].length > index && stacks[i][index] != null && stacks[i][index] != ' ') {
                queues[value.toInt()-1].add(stacks[i][index])
            }
        }
}
 

The sneaky bit was the regex. My first version worked perfectly on the sample data but wasn't even close with the real data set. It turns out I made a subtle error that only became apparent when I switched datasets. My original regex only worked on single digits.

Regex("""move(\d)+ from (\d)+ to (\d)+""")

should have been

Regex("""move (\d+) from (\d+) to (\d+)""")

The parentheses define the digits as a matchable group, so putting the plus (indicating one or more items) causes the part of the input to be matched and affecting the results. With that sorted I could move the proper quantity of crates from the right source to the right destination.

fun moveObjects(queues:Array>, quantity:Int, source:Int, destination:Int) {
    repeat(quantity) {
        // remove from old
        val valueToPop = queues[source-1].pollFirst()
        // push to new
        if (valueToPop != null)
            queues[destination-1].addFirst(valueToPop)
    }
}
 

Part II

In Part II, you were still moving crates but instead of one by one, you needed to move them as a set and preserve initial ordering. To do this, I opted for a double ended queue as temporary storage and added items into it using addLast. When adding to the destination, I added each item to the front of the destination with addFirst but with the source items being repeated calls to removeLast on the temporary storage location. you could have done it with another data structure like a simple array but I'll take readability over terseness and having to track multiple indices.

fun moveCrates(q:Array>, quantity: Int, source: Int, destination: Int) {
    if (quantity == 1) {
        moveObjects(q, 1, source, destination)
    } else {
        val tempQueue = ArrayDeque()
        repeat(quantity) {
            val valueToPop = q[source-1].pollFirst()
            if (valueToPop != null)
            tempQueue.addLast(valueToPop)
        }
        while(tempQueue.isNotEmpty()) {
            q[destination-1].addFirst(tempQueue.removeLast())
        }
    }
}

override fun part02(): Any? {
    queuesClone.forEach{println(it)}
    instructions.forEach {
        moveCrates(queuesClone, it.first, it.second, it.third)
    }

    var topCrates = ""
    queuesClone.forEach {
        val peek = it.peekFirst()
        if (peek != null) topCrates += peek
        else topCrates += " "
    }
    return topCrates
}
 
Permalink

Advent Of Code 2022 - Day 3 - Ruck Reorganization

For day 3, you had to identify objects that were in one or more compartments of a single rucksack or occur across multiple rucksack. A rucksack is a rugged backpack. Its typical usage is in military settings or hiking/backpacking and are usually packed to the brim with items.

Day 3's problem at the core was a string search problem. For Part I, each line of the input was treated as a rucksack with 2 equally sized compartments that you had identify yourself.

vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw
 

In each compartment, you needed to find the character that appears in both and based on that characters id, determine a priority and sum up the priorities. The priorities are:

  • Lowercase item types a through z have priority values 1 to 26, and,
  • Uppercase item types A through Z have priority values 27 to 52.

Part I

I began by substringing the lines into two halves, incurring some off by one errors along the way and then did what amounted to a full random search (n^2 worst case) because the two halves weren't guaranteed to be in any sort of order beforehand.

After getting the matches, I ran through them and subtracted the appropriate constant per the ASCII chart to convert the character to the 1 - 52 range.

fun part1(): Any {
    val common = mutableListOf()

    while (scan.hasNextLine()) {
        val localMatches = mutableSetOf()
        val line = scan.nextLine()
        val len = line.length
        val part1 = line.substring(0, len / 2).toCharArray()
        val part2 = line.substring(len / 2).toCharArray()
        part1.forEach { if (part2.contains(it)) localMatches.add(it) }
        common.addAll(localMatches)
    }
    return common.sumOf {
        if (it.isLowerCase())
            it.toInt() - 96
        else (it.toInt() - 38)
    }
}
 

Part II

For Part II, the goal was now to identify the common item between groups of three rucksacks. That new requirement made the solution for Part I unsuitable for Part II. It already had a slow runtime but the n was small so it was manageable. n^3 time is hard to justify in any circumstance.

To speed things up, I altered the order of my transformations. First, the code to convert the item types into priority ids was spun out into its own function.

fun charToScaledInt(c: Char): Int {
    return if (c.isLowerCase())
        c.toInt() - 96
    else (c.toInt() - 38)
}
 

The next problem to solve was the variable ruck size. This would wreak havoc on the search code as you could easily throw ArrayIndexOutOfBoundsExceptions. The solution was to normalize the ruck strings into IntArrays. The code in processString is essentially the first part of Counting Sort

fun processString(input: String): IntArray {
    val array = IntArray(53)
    input.forEach {
        val index = charToScaledInt(it)
        array[index] += 1
    }
    return array
}
 

The sorted rucks can be searched in linear time with one pass versus n^3. Here's the full part II code:

override fun part02(): Any? {
    var sum = 0
    while (lines.isNotEmpty()) {
        val elf1 = processString(lines.removeFirst())
        val elf2 = processString(lines.removeFirst())
        val elf3 = processString(lines.removeFirst())

        for (i in 0..53) {
            if (elf1[i] > 0 && elf2[i] > 0 && elf3[i] > 0) {
                sum += i
                break
            }
        }
    }
    return sum
}
 
Permalink

Advent Of Code 2022 - Day 4 - Camp Cleanup

Titled Camp Cleanup, Day 4's problem is a genre of problem that shows up a lot for Advent of Code: the numeric range problem.

I had the misfortune of having to do this problem twice. I coded it, got my stars, and then went on a trip without saving the code to version control. So apologies if this entry seems too polished.

Numeric range problems can ask you to determine if ranges overlap, don't overlap, if one is fully contained in the other or what values to add or remove to optimize them somehow.

In Kotlin, you can lean on the IntRange type to assist with these problems.

Part I

For Part I, you need to determine if one range is fully contained in the other. It's a matter of calling contains on the range and checking if both the .first and .endInclusive are contained.

override fun part01(): Any? {
    var count = 0
    while (scan.hasNextLine()) {
        val parts = scan.nextLine().split(",")
        val r1 = parts[0].split("-").map { it.toInt() }
        val r2 = parts[1].split("-").map { it.toInt() }
        val range1 = IntRange(r1[0], r1[1])
        val range2 = IntRange(r2[0], r2[1])

        if (range1.contains(range2.first) &&
            range1.contains(range2.endInclusive) ||
            range2.contains(range1.first) &&
            range2.contains(range1.endInclusive)) {
                count++
        }
    }
    return count
}
 

Part II

For this star, you needed to determine overlap but not full containment. All contained ranges are by default overlapping so by quick mental map you could ballpark that the correct answer would be higher than that of part one.

The logic was largely similar with a loosening of conditions so either the first or end needed to be contained to be deemed overlapping.

override fun part02(): Any? {
    var count = 0
    // part 1 saved computed IntRanges into an array of Pair
    part2Cache.forEach {
        val range1 = it.first
        val range2 = it.second
        if (range1.contains(range2.first) || range1.contains(range2.endInclusive) ||
            range2.contains(range1.first) || range2.contains(range1.endInclusive)) {
                count++
        }
    }
    return count
}
 
Permalink

Advent Of Code 2022 - Day 2 - Rock, Paper, Scissors

Day 2 was the first real problem this month. The scenario was to simulate scoring based on the win conditions of a Rock, Paper, Scissors game.

Your score for each round was based on the sum of the outcome (win = 6pts, lose = 0pts, or draw = 3pts) and the hand shape you chose (Rock = 1pt, Paper = 2pts, Scissors = 3pts).

Part I

My first attempt at Part I seized on the fact that the indicators for Rock, Paper, Scissors were assigned to A, B, and C for the opponent and X, Y, and Z for the player.

I "scaled" the X,Y,Z to A, B, C by subtracting the difference between X and Z. I then did a simple comparison and accounted for the off by one error I'd introduced. It worked fine for the sample cases...

A Y
B X
C Z
 

What I noticed when trying the real data was that the test cases never test Rock vs Scissors. That's the one case where a simple greater than comparison fails. Anticipating that Part II would take some sort of turn and need a more robust solution, I went verbose and made an enum class for the hand shapes storing their possible ids and point values.

enum class Shape(val idOne: Char, val idTwo: Char, val value:Int) {
    ROCK('A', 'X', 1),
    PAPER('B', 'Y', 2),
    SCISSORS('C', 'Z', 3)
}

private fun findShape(i: Char): Shape {
    return Shape.values().first { it.idOne == i || it.idTwo == i }
}
 

Once the shapes are determined, they are passed to determineWinner where I listed out the five possible end states as a bunch of if-else statements.

private fun determineWinner(opponent: Shape, player:Shape): Int {
    val draw = 3
    val won = 6
    return if (opponent == player) {
        draw + player.value
    } else if (opponent == Shape.ROCK && player == Shape.PAPER) {
        won + player.value
    } else if (opponent == Shape.PAPER && player == Shape.SCISSORS) {
        won + player.value
    } else if (opponent == Shape.SCISSORS && player == Shape.ROCK) {
        won + player.value
    } else player.value
}


val lines = mutableListOf()  // for part two

override fun part01(): Any? {
    var sum = 0
    while(scan.hasNextLine()) {
        val line = scan.nextLine().toCharArray()
        lines.add(line)
        val opponent = findShape(line[0])
        val player = findShape(line[2])
        sum += determineWinner(opponent, player)
    }
    return sum
}
 

Part II

The Part II twist was that instead of the second character indicated what you played, it now indicated the outcome and you had to determine what to play to reach that outcome. The scoring algorithm from the first round stayed the same.

I created an enum class called Outcome to allow me to lookup the states. Totally not required but it made the code a bit more readable and pluggable into the Part I code. determinePlay took the opponent's play and the desired outcome to provide the right move. Like with the determineWinner function, it was a small list and easily enumeratable.

enum class Outcome(val id:Char) {LOSE('X'),DRAW('Y'), WIN('Z') }
private fun findOutcome(i: Char):Outcome { return Outcome.values().first { it.id == i }}

private fun determinePlay(opponent: Shape, outcome: Outcome): Shape {
    when (outcome) {
        Outcome.DRAW -> return opponent
        Outcome.WIN -> {
            return when(opponent) {
                Shape.ROCK -> Shape.PAPER
                Shape.PAPER -> Shape.SCISSORS
                Shape.SCISSORS -> Shape.ROCK
            }
        }
        Outcome.LOSE -> {
            return when(opponent) {
                Shape.ROCK -> Shape.SCISSORS
                Shape.PAPER -> Shape.ROCK
                Shape.SCISSORS -> Shape.PAPER
            }
        }
    }
}

override fun part02(): Any? {
    var sum = 0
    lines.forEach {
        val opponent = findShape(it[0])
        val outcome = findOutcome(it[2])
        val player = determinePlay(opponent, outcome)
        sum += determineWinner(opponent, player)
    }
    return sum
}
 

From there, I passed the opponent move and the derived player move into determineWinner from Part I to get the score.

On to Day 3...

Permalink

Advent Of Code Setup and Day 1

It's that time of year again where nerds all around the globe wait for the clock to strike 12AM EST each night in December so that they can earn stars for solving the day's scenarios.

Advent of Code is a bit special for me because it was what I prepped with for my Google interview. Compared to a more rote interview prep site like Leetcode, I enjoy the scenarios the problems present. For some reason, I would freeze if you ask me directly to make some advanced structure but come alive when I have an engaging prompt. I've never completed all stars for a year but last year I got through half of the advent calendar (27 out of 50 possible stars.)

My Setup

Since 2020, my main competition language has been Kotlin and I have a Gradle based setup I've adapted over the years from various sources. Understanding how your infrastructure reads streams of data is extra important. Advent of Code is 70% understanding the problem and 30% understanding how to parse in the data.

The keystone class of my setup is AdventOfCode. It handles initializing the file input, provides overrideable functions for part 1 and 2, and finally adds utility and convenience functions for benchmarking and MD5 hashes. Hashes don't come up frequently but I noticed during my all years prep in 2022 that there were more than a few MD5 related problems.

package adventofcode

import java.io.BufferedInputStream
import java.math.BigInteger
import java.security.MessageDigest
import java.util.*
import kotlin.system.measureTimeMillis

abstract class DayOf2015(day:Int) : AdventOfCode(2015, day)
abstract class DayOf2016(day:Int) : AdventOfCode(2016, day)
abstract class DayOf2017(day:Int) : AdventOfCode(2017, day)
abstract class DayOf2018(day:Int) : AdventOfCode(2018, day)
abstract class DayOf2019(day:Int) : AdventOfCode(2019, day)
abstract class DayOf2020(day:Int) : AdventOfCode(2020, day)
abstract class DayOf2021(day:Int) : AdventOfCode(2021, day)
abstract class DayOf2022(day:Int) : AdventOfCode(2022, day)

open class AdventOfCode(val year: Int, val day:Int) {
    var DEBUG = false
    var scanner:Scanner = Util.initScanner("$year/day${String.format("%02d", day)}.in")
    var testScanner:Scanner = Util.initScanner("$year/day${String.format("%02d", day)}.test")

    open fun part01(): Any? = null
    open fun part02(): Any? = null

    companion object {
        fun mainify(codeDay: AdventOfCode) {
            with(codeDay) {
                println("Year $year, day $day")
                measureTimeMillis {
                    println("Part 1: ${part01()}")
                }.run {
                    println("Part 1 Time: ${this}ms")
                }
                println("-----------")
                measureTimeMillis {
                    println("Part 2: ${part02()}")
                }.run {
                    println("Part 2 Time: ${this}ms")
                }
            }
        }
    }
}

object Util {
    var scanner: Scanner? = null
    fun initScanner(filename:String):Scanner {
        val inputStream = ClassLoader.getSystemClassLoader()
                .getResourceAsStream(filename)
        return Scanner(BufferedInputStream(inputStream))
    }

    fun computeMD5Hash(input:String): String {
        val md = MessageDigest.getInstance("MD5")
        val messageDigest = md.digest("$input".toByteArray())
        val no = BigInteger(1, messageDigest)
        // Convert message digest into hex value
        var hashtext = no.toString(16)
        while (hashtext.length < 32) {
            hashtext = "0$hashtext"
        }
        return hashtext
    }
}
 

I was running a relatively old version of Kotlin last year so bumping to current uncovered a bunch of warnings and errors from language changes.

Day 1 - Part I

Advent of Code Day 1 is meant to be an easy on-ramp and usually is some sort of simple arithmetic or comparison problem.

The theme for day 1 this year was calories and snacks. Elves are traveling to a magical forest and want to make sure they have enough calories to make the trip. In part 1, you needed to find the elf carrying the most calories (and thus who is more likely to have a surplus) and report how many calories they are carrying.

1000
2000
3000

4000

5000
6000

7000
8000
9000

10000
 

In the data file, the snacks each elf is carrying is a series of Ints each on its own line followed by a blank line or the end of file to indicate the end of the snacks the given elf is carrying. I went with the most naive solution by keeping a running sum, pushing it to an array on elf change and finding the maximum sum by running maxOrNull.

val elves = mutableListOf()

override fun part01(): Any? {
    var sum = 0
    while(scan.hasNextLine()) {
        val nextLine = scan.nextLine()
        if (nextLine.isEmpty()) {
            // save sum and clear it
            elves.add(sum)
            sum = 0
        } else {
            sum += nextLine.toInt()
        }
    }
    if (sum != 0) elves.add(sum)
    val max = elves.maxOrNull()
    return max
}
 

I call it naive because it uses O(n) space for storing the totals for each elf and a search time of O(n) for the maxOrNull call. I could have improved the space to O(1) and essentially get the max for free if I modified the code to keep a running count of the maximum so far and did that comparison instead of pusing to the array. However, the naive setup make it easy to tackle Part II.

Day 2 - Part II

For Part II, you need to identify the three elves carrying the most calories and find the sum. With the setup of storing each of the elves total in the array in Part I, all I had to do was sort the array and take the three elves' totals and sum them.

override fun part02(): Any? {
    elves.sortDescending()
    val top3 = elves.take(3)
    return top3.sum()
}
 

All and all, it was a fun day one problem and emblematic of how optimizing part one for speed and memory can make it necessary to refactor it a lot for Part II.

Permalink

How I Use Mastodon

Tags: Mastodon

It's been about two weeks since I've started using Mastodon actively and wanted to reflect on how my usage differs from Twitter. It's not meant to be a guide in the style of "Migrate to Mastodon in six steps" or the like. It's just what works for me in this snapshot of time.

"It's dangerous to go alone / The value of community

The advice "join any instance, you can migrate later" is in the best case neutral advice and in the worst case, just about the last thing you want to tell a person if you want them to actually stay on Mastodon.

Mastodon reminds me of the physical world with each instance being a defacto state or territory. They have their laws you need to abide by but they have open borders. Your first view of the "world" is through the lens of where you live. With your home feed blank, on first joining, you'll see the posts of those on your instance in the Local feed and the people they follow in the Federated feed. Whether or not one or both of these are usable depends on the size of your server.

Humans tend to chunk a single bad experience as emblematic of all. In a centralized system, there's one set of rules (well sorta) that you have to content with. Given how many migrate over from Twitter, picking the "official" server or one at random, it's easy to discover you are in a place that has what feels like strict rules and permabanned from that instance without knowing why. On one hand, I want to acknowledge that some instances might have a deep and pervasive culture of using Content Warnings. But on the other hand, I can see how someone's message asking to use CWs could be received as you want to regulate or tone-police my speech. One of the good outcomes I've seen is when these things happen in error, things seem to be resolved a lot more quickly, amicably, and there's ownership in the process failure.

I lucked out in the instance lottery. My home instance's creators and the first tranche of accounts are folks I've interacted for years so it made me feel less alone or scared I'd violate some contested norm and anchored me to the experience.

No Algorithm == More Engagement ?

One of the things I didn't like about Twitter's feed was knowing that likes would be used as a signal to push content to other folks feeds. [All of the following mentioned without any knowledge of the internals] Just because I interact with someone a lot doesn't mean that all of my interests perfectly overlap with theirs. Seeing a decapitated reply (if I follow both parties in a convo) almost always lacked context. As Favorities don't surface activity to the feeds of followers, I've found myself favoriting more things.

Commenting is another area where I find Mastodon enticing me to participate more even without an algorithm. I really enjoy using the Unlisted option (Visible for all but doesn't surface alone in feeds). Unlisted feels like a semi-private coffee convo versus talking via megaphone. Unless the reply feels especially noteworthy, I use Unlisted and let folks follow the convo from the beginning [I sometimes boost the original toot too]. Think back to how many "Yup", "100%", "I agree" replies would clog up your Twitter feed. Let's not do that on Mastodon.

Bookmark All The Things

The lack of an algorithmic feed means you need to do your own information curation. Hashtags are searcheable but not full-text. So if you see something you want to read later, bookmark it. If you see an interesting post from someone but are unsure you want to follow them, bookmark it.

Of course, bookmarks are a thing on Twitter but I wasn't as organically exposed to people I don't follow (Local and Federated). I've been going through to read the things on occasion.

Hopeful for the Future

While I'm sad about the unfolding turmoil at Twitter and am rooting for the future success of those who have lost their jobs, I'm glad to have a social network I'm happy to post on, feels engaging, and doesn't magnify the negatives of social media that I experienced on Twitter.

Permalink

Much Ado About Virtual Conferences

As in-person conferences make their way onto more schedules, though it hasn't totally eliminated virtual conferences, folks are looking deeper into what they like and dislike about the virtual format.

It's on that backdrop that speaker and conference organizer Austin Parker wrote a post, Virtual Events Are Dead, Long Live Virtual Events, where he discusses some of the ways the format doesn't work for speakers or the audience. I found his argument compelling because he's been a speaker and a conference organizer. On the heels of that post, Lian Li invited Austin to participate in a Twitter Spaces panel titled "Back to normal? Is this the end of virtual tech events".

The panel discussion is a good complement to the blog post but it clocks in at 90 minutes covering a number of viewpoints from almost a dozen participants. They talked about the virtual events they attended, what they like and dislike about the format, its future, and how they might be made better. I wanted to share the info with my team in a form that was a little more digestible so made a zine style sketchnote of it. I added a bit of content about the flipped classroom that one panelist mentioned.

Sketchnote cover image depicting woman on airplane and at home with a dog wanting her to play

My Experiences with Virtual Events and Outlook

I've only given one virtual talk during the pandemic and that was pre-recorded with me in chat responding to questions. It was fine. I've done a lot of recorded videos in my current and previous DevRel roles. One of the things I liked about that previous role were the opportunities we had to be creative. Our video team was open to putting narrative vignettes between instructional modules. Those were often harder to land than the regular content because of costume concerns, how it will read, specific cultural references, etc. But when it lands well, it's really fun.

I think sometimes there is a hesitation to go a little bit out there and instead play to the lowest common denominator.

The only actual "register and block off time" conferences I've gone to were Google I/O and Android Dev Summit. IO was the more immersive of the two with a online "experience" space that you could navigate a character around and interact with.

The first year we had it in 2021, I remember some folks lamenting when the adventure portion was shut down for the year. I had coworkers who spent after hours time securing all the collectibles fishing and bumping into the things. Android Dev Summit was a more generic playlist of talks experience. I watched a bit on the day but most I ended up catching a couple weeks later. I didn't feel the pull to "attend" that I did when it was in person.

I think the virtual genie is out of the bottle and we've realized that while in-person is more optimal, virtual remote events can produce a good experience with enough effort. I do think the number of virtual events will decrease somewhat but am hopeful that we, as a community, work to make them better and rewarding to attendees and speakers. I want to explore speaking at more remote local events and engage with developers that wouldn't usually be able to make it to the events I usually frequent.

Permalink

Fun with Fractals and Flame

Fractals are patterns that are self-similar across scale, meaning a part of the object is similar to the whole. Their earliest applications revolved around simple recursion but today there is a whole field of study for generating fractals and the search to find formulas that will replicate nature. Fractals exist in many parts of nature and biology including trees, DNA, blood vessels, lighting, and some foods.

Before the first computer generated fractals were created, man-made fractals have been noticed in ancient and modern architecture and textiles. In game development, procedural generation of fractals or cellular automata (like Conway's Game of Life) are simple ways to provide everything needed for a game loop without needing a more defined game concept.

Barnsley Fern with many many many points

Sierpinski Carpet

A Sierpinski carpet attains its self-similarity but subdividing itself into a number of copies of the whole, removes one, and recurses on the subdivided copies. One formula to create a Sierpinski carpet is to:

  1. Start with a square.
  2. Subdivide it into 9 squares (3x3)
  3. Delete the center square.
  4. Repeat.

Sierpinski carpet with 1 iteration Sierpinski carpet with 5 iterations

import 'package:flutter/material.dart';
import 'package:flame/game.dart';
import 'package:flame/components.dart';

class SierpinskiCarpet extends FlameGame {
  late Size dimens;
  final RECURSIONS = 5;
  var white = Paint()..color = Colors.white;
  var black = Paint()..color = Colors.black;
  var blue = Paint()..color = Colors.blue;

  double shortestSide() {
    return dimens.width < dimens.height ? dimens.width : dimens.height;
  }
  @override
  Future onLoad() async{
    dimens = canvasSize.toSize();
    double square = shortestSide();
    var bounds = RectangleComponent(position:Vector2(0,0), size:Vector2.all(square), paint:blue);

    // Draw a white square matching the bounds
    add(bounds);

    punchCantorGasket(bounds.position.x, bounds.position.y, bounds.size.x, RECURSIONS);
  }
  void punchCantorGasket( double x, double y, double size, int recursions) {
    // Base case, if recursions = 0, return
    if (recursions == 0) {
      return;
    }

    double newSize = size / 3.0;
    double newSize2 = newSize * 2;
    var newRect = RectangleComponent(position: Vector2(x+newSize, y + newSize), size:Vector2.all(newSize), paint: black);
    add(newRect);

    recursions--;

    // Call punchCantorGasket on all 8 other squares
    punchCantorGasket(x, y, newSize, recursions); // 0,0
    punchCantorGasket(x, y + newSize, newSize, recursions); // 0,1
    punchCantorGasket(x, y + newSize2, newSize, recursions); // 0,2

    punchCantorGasket(x + newSize, y, newSize, recursions); // 1,0
    punchCantorGasket(x + newSize, y + newSize2, newSize, recursions); // 1, 2

    punchCantorGasket(x + newSize2, y, newSize, recursions); // 2,0
    punchCantorGasket(x + newSize2, y + newSize, newSize, recursions); // 2,1
    punchCantorGasket(x + newSize2, y + newSize2, newSize, recursions); // 2,2
  }
}
 

Barnsley Fern

Iterated function systems (IFS) create fractals by taking copies of itself, mutating the copy in some way and then unioned with the rest of the system. First described in the book Fractals Everywhere by its namesake Michael Barnsley, the Barnsley Fern simulates black spleenwort (Asplenium adiantum-nigrum).

Barnsley Fern

Barnsley Fern Formula

The IFS uses 4 transformations each with different probability frequencies shown in the table below.

wabcdefpPortion Generated
ƒ10000.16000.01Stem
ƒ20.850.04-0.040.8501.60.85Smaller leaflets
ƒ30.20-0.260.230.2201.60.07Largest left-hand leaflet
ƒ4-0.150.280.260.2400.440.07Largest right-hand leaflet
The starting point of the system is set to (0,0). The next point is created by calculating a random double and using its value to determine which transformation to apply with the last x and y coordinates resulting in the new location to plot. As more points are plotted, the form of the fern takes shape.
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:async' as Async;

class BarsleyFern extends FlameGame {
  final RADIUS = 0.6;

  final interval = Duration(milliseconds: 200);

  Random rnd = Random();
  Paint leafPaint = Paint()..color = Colors.green;
  Vector2 currentPoint = Vector2.all(0.0);

  @override
  Future onLoad() async {
    // Flame provides its own timer, and we want to use the core Flutter one.
    Async.Timer.periodic(interval, drawLeaves);
  }

  void drawLeaves(Async.Timer t) {
    for (var i = 0; i < 500; i++) {
      currentPoint = calculateNextPoint(currentPoint);
      drawCircle(currentPoint);
    }
  }


  Vector2 calculateNextPoint(Vector2 lastPoint) {
    var r = rnd.nextDouble();
    Vector2 nextPoint = Vector2.all(0.0);
    if (r < 0.01) {
      nextPoint.x = 0;
      nextPoint.y = 0.16 * lastPoint.y;
    }
    else if (r < 0.86) {
      nextPoint.x = 0.85 * lastPoint.x + 0.04 * lastPoint.y;
      nextPoint.y = -0.04 * lastPoint.x + 0.85 * lastPoint.y + 1.6;
    }
    else if (r < 0.93) {
      nextPoint.x = 0.20 * lastPoint.x - 0.26 * lastPoint.y;
      nextPoint.y = 0.23 * lastPoint.x + 0.22 * lastPoint.y + 1.6;
    }
    else {
      nextPoint.x = -0.15 * lastPoint.x + 0.28 * lastPoint.y;
      nextPoint.y = 0.26 * lastPoint.x + 0.24 * lastPoint.y + 0.44;
    }

    return nextPoint;
  }

  void drawCircle(Vector2 point) {
    // Scale point to canvas
    Vector2 plot = Vector2.all(0.0);
    plot.x = canvasSize.x * (point.x + 3) / 6;
    plot.y = canvasSize.y - (canvasSize.y * (point.y + 2)/14);
    add(CircleComponent(radius:RADIUS, paint:leafPaint, position: plot));
  }
}
 
This animation draws 500 points every 200 milliseconds.

Dragon Curve

A Lindenmayer system (L-system) uses an alphabet with production rules to expand the resulting string. Encoded into the alphabet are in instructions like moving a distance, rotating by an angle, or a combination of these rules. Aristid Lindenmayer, the theoretical botanist and biologist for whom they are named, used them to describe the growth of simple organisms such as bacteria. L-systems can be seen in herbaceous plants and trees. His work survives in a posthumously published book, The Algorithmic Beauty of Plants. The Dragon Curve is a fractal made by a single line naively by repeatedly folding each edge towards 90 degree angles. Dragon Curve fractal
Given the grammar:
Variables: F G

Constants: + -

Start: F
Angle: 90

Rules:
F -> F+G
G -> F-G
 
F and G are instructions to draw forward by a scalar amount. + means turn left by angle and - means turn right by that same angle. In the case of dragon curves, you don't need to recursively expand the string using the production rules. An alternate way of producing a dragon curve is to reduce the grammar to draw forward and do a left or right turn at the same time (L and R). You can now make a dragon curve by taking the output of the previous curve, adding a left turn and appending the result of reversing and inverting the previous curve.
StepExpansion
0L
1L + L + R
2LLR + L + LRR
3 LLRLLRR + L + LLRRLR
import 'package:flutter/material.dart';
import 'package:flame/game.dart';
import '../new_shape_components/line.dart';

/*
The Dragon Curve is a fractal made by a single line. It is formed of a series of turns, which can be constructed in the following way:
0: L
1: L + L + R
2: LLR + L + LRR
3: LLRLLRR + L + LLRRLRR
The nth dragon curve is the n-1th dragon curve plus L, plus the n-1th dragon curve reversed and reflected.
In this project we have split up the tasks of generating and drawing the dragon curve into separate classes.
 */

class DragonCurve extends FlameGame{
  final RECURSIONS = 15;

  @override
  Future onLoad() async {
    List dragonCurve = DragonCurveGenerator().generateDragonCurve(canvasSize.x.toInt(), canvasSize.y.toInt(), RECURSIONS);
    add(Polyline(dragonCurve, Paint()..color = Colors.green));
  }
}
enum Direction { LEFT, RIGHT}

class DragonCurveGenerator {
  Vector2 turn(Vector2 heading, Direction turn) {
    var newHeading = Vector2.all(0.0);
    if (turn == Direction.LEFT) {
      newHeading.x = -heading.y;
      newHeading.y = heading.x;
    } else {
      newHeading.x = heading.y;
      newHeading.y = -heading.x;
    }
    return newHeading;
  }

  List dragonTurns(int recursions) {
    List turns = [];
    turns.add(Direction.LEFT);

    for(int i = 0; i < recursions; i++) {
      // Add a left turn to turns
      turns.add(Direction.LEFT);
      // Add reflected version of reversed to turns
      for(int j = turns.length-2; j >= 0; j--) {
        if (turns[j] == Direction.LEFT)
          turns.add(Direction.RIGHT);
        else turns.add(Direction.LEFT);
      }
    }
    return turns;
  }

  List generateDragonCurve(int width, int height, int recursions) {
    var turns = dragonTurns(recursions);

    var head = Vector2(width/2, height/2);
    var heading = Vector2(5, 0);

    var curve = [];

    curve.add(Vector2(head.x, head.y));
    turns.forEach((turnInstruction) {
      heading = turn(heading, turnInstruction);
      head.x += heading.x;
      head.y += heading.y;
      curve.add(Vector2(head.x, head.y));
    });
    return curve;
  }
}
 

A lot of fractals will have multiple means to produce them.

Permalink