James Williams
LinkedInMastodonTwitterGithub

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...

Link to Full Article

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.

Link to Full Article

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.

Link to Full Article

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.

Link to Full Article

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.

Link to Full Article

Creating New Components with Flame

In the last post, we worked through drawing and styling a couple of basic components in Flame. In this post, we'll create some new ShapeComponents and composite components.

How ShapeComponents are derived

The components we've used so far, RectangleComponent, CircleComponent, TextComponent all descend from PositionComponent allowing them to be moved around the screen, scaled, or rotated. CircleComponent and RectangleComponent inherit the ability to represent a geometric shape with a size and rotation angle from ShapeComponent.

Diagram showing the relationship between various Flame components

Two of the shapes that were missing when I started working with the library are lines and arcs.

A ShapeComponent must implement two functions: render(Canvas canvas) that draws your shape and containsPoint(Vector2 p) that will determine if a given point is within the shape's hitbox.

The Canvas is from the dart:ui package in core Dart. There are a lot of methods that aren't exposed directly to Flame.

Creating an Arc

There's already a function for drawing an arc in Flutter Canvas `drawArc`` that we can use encapsulate into a new ShapeComponent.

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

class Arc extends ShapeComponent {
  Vector2 pos;
  double radius = 5.0;
  double startAngle = 0.0;
  /*  Start angle begins at right center
      and moves counterclockwise
      +----270---+
      |          |
      180        0
      |          |
      +----90----+
   */


  double sweepAngle = 360.0;
  bool isClosed = true;
  Paint paint = Paint()..color = Colors.green;

  Arc({required this.pos, required this.radius, required this.startAngle, required this.sweepAngle, required this.paint}) : super(
    position: pos,
  ) {
    add(CircleHitbox(position:this.pos, radius:this.radius));
  }

  @override
  bool containsPoint(Vector2 p) {
    // TODO: implement containsPoint
    throw UnimplementedError();
  }

  @override
  void render(Canvas canvas) {
    Rect r = Rect.fromLTWH(pos.x, pos.y, radius, radius);
    canvas.drawArc(r, startAngle * pi/180, sweepAngle * pi/180, isClosed, paint);
  }
}
 

The Arc class acts mostly as a wrapper for the properties needed for canvas.drawArc. I'm not doing any collision detection so the implementation of containsPoint is empty.

After creation, we can use the Arc like any other ShapeComponent.

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

class CirclesAndArcs extends FlameGame {
  final strokePaint = Paint()
    ..color = Colors.red
    ..style = PaintingStyle.stroke;


  @override
  Future onLoad() async {
    add(
        CircleComponent(radius: 8.0, position: Vector2.all(100),
          paint:Paint()..color = Colors.white));

      var arc = Arc(pos:Vector2(150,100), radius: 50, startAngle:270.0, sweepAngle:270.0, paint:Paint()..color = Colors.green);
   add(arc);

    var arc2 = Arc(pos:Vector2(200,100), radius: 50, startAngle:0.0, sweepAngle:90.0, paint:strokePaint);
    arc2.isClosed = false;
    add(arc2);
  }
}
 

Creating Lines

If I needed to draw a single line, RectangleComponent would work just fine. For a component that joins every new segment to the end of the last segment, you need a new ShapeComponent.

A spiral drawn using line segments

The implementation of a Line and Polyline are similar to that of an arc, just enough to wrap the Canvas methods. LineShape draws a single line and Polyline draws a continuous line using a list of Vector2 vertices using each successive point to draw a line from the previous point.

import 'package:flame/src/game/notifying_vector2.dart';
import 'package:flame/components.dart';
import 'package:flame/geometry.dart';

import 'package:flutter/material.dart';

class LineShape extends ShapeComponent {
  LineSegment segment;
  NotifyingVector2 _position;
  double angle = 0.0;

  final DEFAULT_LINE = LineSegment(Vector2(0.0, 0.0), Vector2(1.0, 0.0));

  LineShape(this.segment, this._position, this.angle)
      : super(angle: angle, position: _position, size: Vector2.all(1));

  @override
  bool containsPoint(Vector2 point) {
    return false;
  }

  @override
  void render(Canvas canvas) {
    canvas.drawLine(segment.from.toOffset(), segment.to.toOffset(), paint);
  }
}

class Polyline extends ShapeComponent {
  var points = [];
  Paint paint = Paint()..color = Colors.red;

  Polyline(this.points, this.paint);

  @override
  bool containsPoint(Vector2 p) {
    return false;
  }

  @override
  void render(Canvas canvas) {
    for (int i = 0; i < points.length - 2; i++) {
      canvas.drawLine(points[i].toOffset(), points[i + 1].toOffset(), paint);
    }
  }
}
 

import 'package:flame/game.dart';
import 'package:flame/palette.dart';
import 'package:ud405_flame/stories/new_shape_components/line.dart';

class DrawASpiral extends FlameGame {
  final COILS = 20;

  @override
  Future onLoad() async {

    final green = BasicPalette.green.paint();

    final screenWidth = canvasSize.x;
    final screenHeight = canvasSize.y;

    final xStep = screenWidth / 2 / COILS;
    final yStep = screenHeight / 2 / COILS;

    var points = [];

    for (int i = 0; i < COILS; i++) {
      final xOffset = xStep * i;
      final yOffset = yStep * i;

      var point1 = Vector2((xOffset - xStep), yOffset);
      var point2 = Vector2((screenWidth - xOffset), yOffset);
      var point3 = Vector2((screenWidth - xOffset), (screenHeight - yOffset));
      var point4 = Vector2(xOffset, (screenHeight - yOffset));
      var point5 = Vector2(xOffset, (yOffset + yStep));

      points.addAll([point1, point2, point3, point4, point5]);
    }
    add(Polyline(points, green)); //, paint:green));
  }
}


 

Creating a composite Component

Next, we will create a reusable composite component composed of several ShapeComponents, a flower.

Our flower consists of a number of rectangles: - a long green rectangle for the stem, - two rotated green rectangles for the leaves, and, - set of rotated rectangles acting as petals.

Rectangular flower

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

import 'dart:ui' as ui;

class RectangularFlower extends FlameGame {


   final gradientPaint = Paint()
      ..shader = ui.Gradient.sweep(
        Offset(0, 0),
        [
          Colors.yellow.withOpacity(0.8),
          Colors.purple.withOpacity(0.4),
          Colors.deepPurple.withOpacity(0.9),
        ],
        [
          0.0,
          0.3,
          1.0,
        ],
       );

  @override
  Future onLoad() async {
    add(RectFlower(Vector2(100,0.0), Paint()..color = Colors.yellow));

    add(RectFlower(Vector2(300, 0), gradientPaint));
  }
}

class RectFlower extends PositionComponent {
  var green = Paint()..color = Colors.green;
  var darkGreen = Paint()..color = const Color(0xDD00CC32);
  var white = Paint()..color = Colors.white;
  var yellow = Paint()..color = const Color(0xFFEBE834);

  RectFlower(Vector2 position, Paint petalPaint){
    // Draw stem
    var stem = RectangleComponent(position: Vector2(48 + position.x, 100 + position.y), size: Vector2(10, 200), paint: green);
    add(stem);
    // Draw leaves
    var leaf1 = RectangleComponent(position: Vector2(40 + position.x , 180 + position.y), size: Vector2(40, 40), angle: 45, paint:darkGreen);
    var leaf2 = RectangleComponent(position: Vector2(85 + position.x, 230 + position.y), size: Vector2(40, 40), angle: 45, paint: darkGreen);
    add(leaf1);
    add(leaf2);

    // Draw petals
    const PETALS_COUNT = 20;
    for (int i = 0; i < PETALS_COUNT; i++) {
      var petal = RectangleComponent(position: Vector2(50 + position.x, 100 + position.y),
        size: Vector2(40, 40), angle: 360 * i/PETALS_COUNT, paint:petalPaint);
      add(petal);
    }
  }
}
 

RectFlower bases the locations of its child components rather arbitrarily from the top left corner. In its current state, the component isn't interactive and won't be able to respond to any bounds checking because the size defaults to zero.

In the game class, we can instantiate a couple of flowers and give their petals each a different paint.

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

import 'dart:ui' as ui;

class RectangularFlower extends FlameGame {


   final gradientPaint = Paint()
      ..shader = ui.Gradient.sweep(
        Offset(0, 0),
        [
          Colors.yellow.withOpacity(0.8),
          Colors.purple.withOpacity(0.4),
          Colors.deepPurple.withOpacity(0.9),
        ],
        [
          0.0,
          0.3,
          1.0,
        ],
       );

  @override
  Future onLoad() async {
    add(RectFlower(Vector2(100,0.0), Paint()..color = Colors.yellow));

    add(RectFlower(Vector2(300, 0), gradientPaint));
  }
}

class RectFlower extends PositionComponent {
  var green = Paint()..color = Colors.green;
  var darkGreen = Paint()..color = const Color(0xDD00CC32);
  var white = Paint()..color = Colors.white;
  var yellow = Paint()..color = const Color(0xFFEBE834);

  RectFlower(Vector2 position, Paint petalPaint){
    // Draw stem
    var stem = RectangleComponent(position: Vector2(48 + position.x, 100 + position.y), size: Vector2(10, 200), paint: green);
    add(stem);
    // Draw leaves
    var leaf1 = RectangleComponent(position: Vector2(40 + position.x , 180 + position.y), size: Vector2(40, 40), angle: 45, paint:darkGreen);
    var leaf2 = RectangleComponent(position: Vector2(85 + position.x, 230 + position.y), size: Vector2(40, 40), angle: 45, paint: darkGreen);
    add(leaf1);
    add(leaf2);

    // Draw petals
    const PETALS_COUNT = 20;
    for (int i = 0; i < PETALS_COUNT; i++) {
      var petal = RectangleComponent(position: Vector2(50 + position.x, 100 + position.y),
        size: Vector2(40, 40), angle: 360 * i/PETALS_COUNT, paint:petalPaint);
      add(petal);
    }
  }
}
 

Next time, we'll create some fractals.

Link to Full Article

Making Games with Flutter and Flame

Like many people who become computer science majors, I wanted to make games as a kid. I even took a couple courses and fell accidentally into writing a book on HTML5 Game Programming. Later at Udacity, I was able to architect a short lived game programming credential using libGDX. And then I promptly did nothing for like five years.

The pandemic sparked an opportunity to revisit that hobby. Taking stock of the content I have, much of the HTML5 stuff is broken or uses libraries that don't exist anymore. The libGDX stuff is likewise not functional. That one is not because of the library itself. libGDX is alive and well but there have been lots of changes in Gradle (build system). It would be quite a bit of work update all the project files. Learning new stuff is more fun than maintenance so I'm learning a new framework, Flame. With the announcement of the Flutter Casual Games Toolkit at I/O this year, the choice seems serendipitous.

What is Flame?

Flame is a 2D game engine built on top of Flutter. Flame has a number of extension packages for things like audio, physics, collision detection, and level editing. Mobile, web, are desktop platforms are supported. It uses a component model where each component manages its own state and respond to events and draw calls.

Flame Components similar to Flutter and Widgets. They are the building blocks of your game.

Hello World

The main entry point for our examples, FlameGame is also a Component.

One of the first things I tried to do was to draw text. I initially feared it would be difficult once you start using different fonts. Sometimes that's the case in game frameworks, the default font is easy but you have to do all this . Fortunately, TextComponents derive much of their format from core Flutter. In the pubspec.yaml of a typical Flutter project, you'd specify the locations and common names for the fonts you intent to use.

Here is a portion of the pubspec.yaml declaring the fonts we'll use in the project.

  fonts:
     - family: Cinzel
       fonts:
         - asset: fonts/Cinzel-Medium.ttf
     - family: Corinthia
       fonts:
         - asset: fonts/Corinthia-Regular.ttf
     - family: Redacted Script
       fonts:
         - asset: fonts/RedactedScript-Regular.ttf
 

The game draws random strings in random locations around the screen using the above fonts.

Drawing various random words

TextComponent takes a text string and a TextPaint textRenderer. WordFactory selects a random word from the list and selects a random color and randomizes which preset font to display.

import 'dart:math';

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

import 'package:flutter/material.dart';

class WordCloud extends FlameGame {
  @override
  Future onLoad() async {
    const WORD_COUNT = 20;
    var factory = WordFactory(this.canvasSize);
    for (var i = 0; i < WORD_COUNT; i++) {
      add(factory.generateWord());
    }
  }
}

class WordFactory {
  Vector2 canvasSize;

  WordFactory(this.canvasSize);

  var rnd = Random();
  static const WORDS = [ /* List of words here */  ];

  TextPaint? generateRandomTextPaint() {
    var color = Color((rnd.nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
    var fontNum = rnd.nextInt(4);
    var fontSize = rnd.nextInt(48).toDouble();
    if (fontSize < 24)
      fontSize = 24;
    switch (fontNum) {
      case 0:
        return TextPaint(style: TextStyle(fontFamily: 'Redacted Script',color: color, fontSize: fontSize));
      case 1:
        return TextPaint(style: TextStyle(fontFamily: 'Cinzel' ,color: color, fontSize: fontSize));
      case 2:
        return TextPaint(style: TextStyle(fontFamily: 'Corinthia', color: color, fontSize: fontSize));
      default:
        return TextPaint(style: TextStyle(color: color, fontSize: fontSize));
    }
  }

  TextComponent generateWord() {
    var word = WORDS[rnd.nextInt(WORDS.length)];
    var x = rnd.nextInt(canvasSize.x.toInt());
    var y = rnd.nextInt(canvasSize.y.toInt());
    return TextComponent(text: word, textRenderer: generateRandomTextPaint())
      ..x = x.toDouble()
      ..y = y.toDouble();
  }
}
 

You can expect to spend as much time in general Flutter documentation as you will searching Flame APIs.

Drawing A Starfield

For the next example, I wrote a small bit of code to draw a bunch of components on screen. There isn't a PointComponent in Flame but a point is really just a very small rectangle. Using the canvas size and a desired density of points, the code creates a list of randomized points and adds them to the component tree.

Starfield

import 'dart:math' as math;

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


class DrawAStarfield extends FlameGame {
  final starPaint = Paint()..color = Colors.white;

  final density = 0.2;
  var stars = [];

  @override
  Future onLoad() async {
    var canvasSize = this.canvasSize.toSize();
    var width = canvasSize.width;
    var height = canvasSize.height;
    var size = 10000 * density;
    var r = math.Random();

    for (int i = 0; i < size; i++) {
      var x = r.nextDouble() * width;
      var y = r.nextDouble() * height;
      stars.add(Vector2(x, y));
    }

    for (int j = 0; j < size; j++) {
      add(RectangleComponent(position: stars[j], size: Vector2(1, 1),
          paint: starPaint));
    }
  }
}
 

Drawing Lines

DrawLines is pretty much the previous snippet but with more paints. First a couple different colors and then a LinearGradient with various colors as the stop points.

Lines using various Paints

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

import 'dart:ui' as ui;

class DrawLines extends FlameGame {
  final white = Paint()..color = Colors.white;
  final magenta = Paint()..color = Color(0xFFFF00FF);
  final red = Paint()..color = Colors.red;
  final blue = Paint()..color = Colors.blue;
  final green = Paint()..color = Colors.green;

  @override
  Future onLoad() async {
    add(
        RectangleComponent(position: Vector2(0, 100), size: Vector2(500, 2), angle: 45,
            paint:white));
    add(
        RectangleComponent(
            position: Vector2(150, 100), size: Vector2(500, 2), angle: 45,
            paint:magenta));

    add(
        RectangleComponent(
            position: Vector2(200, 100), size: Vector2(500, 2), angle: 45,
            paint:green));

    final gradientPaint = Paint()
      ..shader = ui.Gradient.linear(
        Offset(70, 100),
        Offset(500, 20),
        [
          Colors.green.withOpacity(1.0),
          Colors.blue.withOpacity(0.2),
          Colors.yellow.withOpacity(0.8),
          Colors.red.withOpacity(0.1),
          Colors.red.withOpacity(0.9),
        ],
        [
          0.0,
          0.5,
          0.7,
          0.9,
          1.0,
        ],
      );

    add(RectangleComponent(
        position: Vector2(70, 100), size: Vector2(500, 20), angle: 45,
        paint:gradientPaint));
  }
}
 

If you don't already have good Flutter fundamentals, working with Flame will definitely get you up to speed.

Link to Full Article

Emacs as an IDE: Android, Java, and Kotlin

In the two previous posts, I covered basic Emacs setup and Flutter support. I'll now be tackling the other area that consumes most of my work development time, Java/Kotlin and Android.

Installing Java, Kotlin, and Gradle with SDKMAN

My preferred way to install Java and Kotlin is sdkman.io which allows you to install several Java and Kotlin versions along side each other and easily switch between them. To run Android projects, you only specifically need to install Java which all versions of Gradle requires to run. Many Android projects will include a gradlew script that will download Gradle at build time but I like to install a specific version of Gradle because the wrapper will redownload executables and dependencies.

sdk i java 11.0.13.fx
sdk i kotlin
sdk i gradle

Latest and greatest is generally fine for Kotlin and Gradle. For Java, I like to use JDK11.

Adding Java, Kotlin and Android support to Emacs

Again, we venture to Doom's init.el file (SPC f P), we need to add enable some options.

In the :lang section, we need to modify the java and kotlin lines as follows:

(java +lsp)
(kotlin +lsp)

Enabling the Java module enables Java, Groovy, and Android support. The Android features allow you to create, control, and debug Android projects from Emacs with some keybindings for common operations. You can even control emulators. The Kotlin module provides a Kotlin REPL and its own Gradle support.

Setting up Java and Kotlin LSPs

Java's LSP server is a part of the default doom-emacs build so all you need to do to install is executing M-x lsp-install-server and select jdtls.

If you change Java versions, you need to uninstall and reinstall the server.

M-x +lsp/uninstall-server
M-x lsp-install-server

Kotlin, on the other hand, requires a little bit more work. LSP support is provided via a community developed LSP server at https://github.com/fwcd/kotlin-language-server.

Clone the repo, run gradlew :server:distZip to get an archive of the language server and its dependencies. The compiled zip will be in the /server/build/distributions folder. Extract that somewhere and add the path to your config.el doom file. Add the Java path for good measure

(add-to-list 'execpath "<path to kotlin-language-server>")
(setenv "JAVA_HOME" "<java home path>")

Finally, reload(SPC h r r) your configuration and you are good to go. Here's a quick video showing how moving around a project would work.

Link to Full Article

Emacs as an IDE: Flutter

In the last post, we did the basic quality of life setup to make Emacs into a general purpose supped up text editor. This post will walk you through setting up Flutter IDE support rivaling that of VS Code or Android Studio.

Before starting you should install Flutter using the instructions on flutter.dev. Make a note of where Flutter is installed on your machine. Some things will work out of the box and others will require paths to be specified.

Adding Flutter and Dart support to Emacs

Again we need to go back to the Doom init.el file (SPC f P) and add enable some options.

In the :lang section, uncomment dart and add +lsp and +flutter. Dart and Flutter use yaml configuration files for dependencies so you can optionally uncomment yaml to enable some basic highlighting.

(dart +flutter +lsp)   ; paint ui and not much else
;; ...
yaml                   ; JSON, but readable

Even if you are able to run Flutter or Dart from a command-line prompt, Emacs doesn't always pick up your path so it's best to specify them. We'll need to declare the following variables:

lsp-dart-sdk-dir          ;; location of Dart SDK
lsp-dart-flutter-sdk-dir  ;; location of Flutter SDK
flutter-sdk-path          ;; location of Flutter SDK

The Flutter SDK needs to be defined for both twice because under the covers there are two different tools providing Flutter support. Given that FLUTTER_DIR is the base directory of your Flutter install, add the following to your config.el file.

(setq lsp-dart-sdk-dir "FLUTTER_DIR/bin/cache/dart-sdk")
(setq lsp-dart-flutter-sdk "FLUTTER_DIR")
(setq flutter-sdk-path "FLUTTER_DIR")

Finally, reload(SPC h r r) your configuration and you are good to go. You can read more about the features of lsp-dart.

At this point most commands you could do in say Android Studio are some variant of lsp-dart-* with mostly key bindings for testing. Running a Flutter app or doing a hot reload is bound to SPC m r. Some of the ones I use a lot that don't yet have bindings are:

lsp-dart-pub-get                ;; executes pub get or flutter pub get
lsp-dart-pub-upgrade            ;; executes pub upgrade or flutter pub upgrade
lsp-dart-show-outline           ;; displays the structure of the Dart file
lsp-dart-show-flutter-output    ;; displays Flutter widget structure

Here's how my flow looks when working with Flutter.

Link to Full Article

Emacs as an IDE: Setup

I found myself in emacsland through somewhat unconventional means. It wasn't a parent, mentor, or king of the geeks that turned me on to it, it was grad school. And it was one class in particular, Analysis of Algorithms. To complete the homework assignments for the class, I had to create lots and lots of mathematical equations, graphs, and diagrams. Drawing them by hand was acceptable but no one wants to draw a five or six level diagram by hand.

The one sensible way to do that was LaTeX, a typesetting language created in the 1980s but still in use for print technical books. Emacs org-mode, a likely subject for a subsequent post, has extensive LaTeX support and my time to complete homework was cut by a third.

Application of B-Tree Insert Algorithm

From there I branched out into the other capabilities of emacs, taking advantage of its high level of customization, rich set of plugins, and existence on almost every modern computing platform.

I'll answer the burning question in advance, "why not VS CODE?" I started using emacs before VS Code was a thing. I also like having everything under one roof. Org-mode is an emacs-exclusive feature that I use for document creation and personal productivity. So I'd be spending time here anyways.

Emacs users often share their configurations online in dotfiles repos and there's a culture of learning by example vs formal education. Packages for almost every purpose imaginable are available in various repositories so the joke about users endlessly tweaking their configuration can ring true. This series of posts will not go to far down that path. Mind you, I have been that user but have trimmed down the hand rolled stuff. There will be opportunities to tweak things but we'll be starting from an opinionated base "distro" (a collection of plugins, platforms and processes) called doom-emacs.

Why doom ?

First it's to combat bad ergonomics of a default Emacs config. Long story short, Emacs uses the CTRL and ALT keys a lot for UI navigation. These keys are often at the extreme left and right of the keyboard and a reduced size key as well. There are a lot of other examples where the application shows its age, pre-dating many of the conventions of the last 40 years.

For example, in normal Emacs, you save a file with CTRL+x CTRL+s. In doom-emacs, saving a file is completed with SPC f s. Space is the system wide leader key that brings up a popup window. f is the context for file operations and s saves the file. This organization system makes it easier to discover commands quickly.

Doom emacs menu subsystem

Doom provides a couple different ways to install packages: through normal repositories like MELPA or using its managed module system. Each module will specify packages to install if that module is enabled, associated functions and commands that should be available to the user and binds them to context menus. For example, when you enable the Java module, it knows that most Java builds use Gradle/Groovy so those are installed along side the Java packages.

Doom is also really fast. With a spartan build, it is ready to use in a couple seconds, my full build loads in just under 10s. Much much much faster than I could do with IntelliJ.

Install Emacs and doom-emacs

Doom has a pretty good guide to install Emacs and doom.

It covers the quirks of getting Emacs installed on different platforms and the two lines of code to get doom installed:

git clone https://github.com/hlissner/doom-emacs ~/.emacs.d
~/.emacs.d/bin/doom install

The doom command is your main way to update and sync your installed packages as well as troubleshoot problems with your install(doctor). After this first instance, you can update, sync, and reload your configuration from inside Emacs.

Understanding the module system

Your doom configuration lives in three files: init.el, config.el, and packages.el. Most of the time, you'll be editing init.el as it controls which modules are installed. Modules are grouped by general function areas. Here is a subset of the module groups, listing only the ones we care about.

  • input, alternate keyboard layouts and Japanese/Chinese input
  • completion, code completion and file/command search manipulation
  • ui, modules that modify the general app user interface
  • editor, modules that make Emacs more IDE like (snippets, text manipulation, cursor niceties, etc)
  • emacs, core emacs functionality
  • term, options to run a terminal in Emacs
  • checkers, syntax and grammar checkers
  • tools, general small to medium scale apps and integrations (git, docker, password managers, etc )
  • lang, programming language support
  • app, large scale apps that tailor emacs to that purpose like social media clients, RSS readers and media players

You can peruse the list by running SPC f P. The capital P is important.

Navigation and working with files

Here are a couple of basic doom emacs commands.

Doom emacs File submenu

  • Open file SPC f f
  • Save file SPC f s
  • Open configuration files SPC f P
  • Change to insert mode i
  • Exit insert mode ESC
  • Run a command M-x (read as Meta, it's usually the Alt key)

Emacs needs to differentiate between when you want to edit the contents of a file and when you want to enter commands. You can tell if you are in insert mode or not based on the cursor shape. | is insert mode, █ is command mode. Press i to start editing files.

Setting up basic IDE features

By default, Doom enables a number of defaults. To make it more like a traditional IDE, we can add a project drawer for easy navigation, tabs to switch between files in a project more easily, github style emoji and terminal support. We can also enable a side minimap to navigate inside a file quickly.

In the :ui section of init.el, uncomment/add [remove the ;;]:

minimap
tabs
treemacs
(emoji +github)

In the :term section, uncomment:

term

Exit insert mode (ESC), press SPC f s to save the file and then SPC h r r or alternatively run M-x doom/reload to reload the config.

An project will look like this:

Treemacs and Minimap in emacs

Pretty cool but we add some language support.

Tweaking fonts and themes

Emacs can use an installed system font as the editor font. The default is perfectly serviceable but I like to use Fira Code.

The font is set in the config.el file, usually commented out at first. The following sets 16pt Fira Code Retina as the default UI font.

(setq doom-font (font-spec :family "Fira Code Retina" :size 16))

Doom has over 70 included themes, you can apply a different one using M-x load-theme or SPC h t.

doom-one is the default, check this repo for descriptions of the included themes.

You may get the prompts Loading a theme can run Lisp code. Really load? and Treat this theme as safe in future sessions?. Answer yes to both.

This will activate the theme for only the current session, after which it will revert back to doom-one. To make it permanent, update the theme in the config.el file. The following assigns doom-dark+ permanently.

(setq doom-theme 'doom-dark+)

Language Server Protocol and Debugging

Some of the key features that separates the text based editors like emacs and vim from more heavy IDEs like Android Studio or XCode are things like refactoring, linting and code completion. In the past, you had to forgo all that if you wanted a lightweight portable solution. That's no longer the case.

The Language Server Protocol allows a server to be notified when changes in a document happen. These changes are analyzed and the client is sent a message noting any errors or warnings that have come with the change. The language server can also include possible linting optimizations the user can perform as well as navigating to signatures/defintions of classes and functions.

LSP started as a Microsoft Visual Studio Code feature but developed as its own specification and can be used in almost any text editor that's capable of sending and consuming JSON-RPC messages.

LangServer.org maintains a list of well supported LSPs. I'll be covering the specifics for setting up support for the languages individually but for now, we need to set up global support. In tools section of init.el, enable:

:tools
;; ...
lsp
(debugger +lsp)

Company-mode, short for complete anything is a text completion framework that you get for free with enabling doom's LSP support.

The Debug Adapter Protocol, modeled somewhat like LSP, is a means to provide language and tooling debugging to users. Tooling only needs the common interface of debug adapters. Each debug adapter then handles the specifics of interacting with the underlying debugging tool. Emacs dap-mode hasn't hit 1.0 so it doesn't have as much language coverage as LSP.

The following blog posts will cover the various programming languages and writing environments I use. First up is Flutter.

Link to Full Article