Showing posts with label flutter. Show all posts
Showing posts with label flutter. Show all posts

Monday, April 7, 2025

Ludum Dare 57: A Weekend with Flame

[UPDATE 1 on April 7: Added paragraph about effects and tweens.]

I am still working on the Flutter-powered game that I mentioned before, although I had to put it down for about three weeks while I worked on a departmental report. This past week, I was able to get back into working on other things, and I was somewhat surprised to see Ludum Dare 57 coming up. I am glad the organizer was able to get the support he needed to run the event. On Friday, before the jam started, I spent several hours tinkering with Flame. It is a Flutter-powered game engine that I have known about for some time but, until Friday, had never tinkered with. I pieced together a minimal arcade-style interaction following my usual inspiration: Every Extend. It was enough of an exploration that I figured I could try using Flame for Ludum Dare, even though I knew it would be slow going.

At 9PM Friday, the theme was announced: depths. I sat with a paper and pencil and started doing some doodling. As I drew out some screens for a rather silly concept for a fishing game, into my mind came the Bonzo Dog Band's "Straight From My Heart." The juxtaposition of these two tickled my fancy, and I decided to go with it.

My hope was to spend most of Saturday on the game and then be done with it. Alas, it was a lot slower going than I had anticipated: I ended up spending all Saturday and much of Sunday on the project, and I still didn't get to some of the juice that I had thought of as essential for the experience. Of course, if the goal was to make the game as well as I could, I would have used Godot Engine. My goal, instead, was to learn as much Flame as I could to finish a project within a 48-hour window, and that, I did.

You can check out Fish Face on its Ludum Dare page or go directly to the web-playable version. The source code is on GitHub. The title of the game is a bit on the nose, and I had originally wanted the entire experience to be more surreal. I would have then given it a name like It's a baseball cap on top of an umbrella, but maybe I can save that for my next surrealist project.

Before I got into the technical implementation, let me say a little about the music, which may be my favorite part. I used to do more songwriting, but these days, I only ever eke something out during a game jam. A student pointed me toward some different SoundFont files, and I had downloaded a few last week to tinker with. That tinkering came during Ludum Dare 57, when I adapted the chord progression, transposed, from "Straight From My Heart" into a doo-wop ballad. This arrangement entirely used the Arachno soundfont. Curiously, in my head, this was the actual instrumentation of the Bonzos as well, and it was only after going to the recording that I realized it's primarily guitar, bass, and drums, with just a little sax and then, later, Hammond organ. It was intentional though that my arrangement could be played by the Bonzos. I hope Neil Innes doesn't mind the rhythmic eighth notes the piano got stuck with. In any case, I was really happy to arrange something with a diminished chord, an augmented chord, and modulation.

As for Flame, I will share my experience as some observations about "pros and cons." Keep in mind that this is my first experience with it. Much of the friction undeniably comes from my trying to think about the problem in the way the Flame architects intend it to be used. My intuition at this point is to approach it the way that Godot Engine would handle it, but while there are similarities, it is not the same.

Benefits of Flame

Flame uses the composition of components to build a scene tree in a manner similar to Godot Engine's nodes. This made it easy for me to design a state machine for the main gameplay following the classic State design pattern. I made an abstract State class that was itself a component, and I made my GameWorld have only one of these instances at a time. By mounting the current state as a child component of the GameWorld, its methods were automatically called by the system without my having to delegate from GameWorld. For example, the individual state's onLoad and update methods were called by virtue of its being in the component hierarchy.

One particularly useful type of component is Effect, which can be used to add all sorts of useful transformations. Progression through the transformation is handled by an EffectHandler, so doing something common like an easing function is done with a CurvedEffectHandler that uses an ease-in curve. Effects provided an easy way to get simple animations in place. Running effects in parallel was a simple matter of adding multiple effects, and running them in series was made easy with SequenceEffect.  I especially liked RemoveEffect as well, which comes at the end of my animations that toss face parts off of the screen. It wasn't until after the jam that I realized that Flame's effects fill the same niche as Godot's Tweens: you can stick an arbitrary effect on an object and then forget about it or, if needed, get a notification when complete. Comparing the two shows an interesting difference: Tweens can operate on any property because GDScript is interpreted, while Effects have the benefit of static typing and clear factory constructor names.

Individual components can be augmented through mixins. Dart is the first language I have used significantly that incorporates mixins, and I feel like they are not yet in my arsenal as a software designer. Flame gave me a good excuse to see them in action. For example, my CatchState is where the player must either keep or toss a face part. By giving it the KeyboardEvents mixin, this state now can implement onKeyEvent. I like how explicit this is and how it prevents the superclass from having more methods than necessary. I need to think more about whether this is better or worse than an approach like Godot's, where any node can process input.

Mixins are also used for dependency injection via HasGameRef. This mixin makes the corresponding class have access to a reference to the game, which prevents one from either having a global variable (with the corresponding spaghetti) or passing an explicit reference around all the time (with the corresponding noise). It makes me wonder if there are places in my Flutter game project where my code would be made simpler by using mixins of my own devising, and this inspires me to dig more into the literature on best uses of mixins.

As I have mentioned before, refactoring is comfortable and seamless in a statically-typed language with a powerful IDE. Easy renaming is so important that it should be a presumption of an environment Anything else introduces friction into the already difficult problem of evolving good designs. Similarly, Dart's library-based privacy is convenient for rapid development and code evolution. I can quickly add new classes to one file, and I can keep these private to that file. Classes can then be pulled out into other files as needed.

Flame allows one to use Flutter for UI elements, which should be a greater strength given the power of Flutter and the frustrations of UI work, but this strength was not demonstrated well in Fish Face. I came into the project with a backwards understanding, thinking of Flutter UIs as being added to a Flame game, but I think now it's the other way around: you add a Flame game to a Flutter app. Honestly, I am still having trouble figuring out what this means in terms of design trade-offs and best practices. There was a very helpful chap on the Flame Discord who pointed me in the right direction, but I still feel like I don't have a good understanding of how, where, and when to bring these two worlds together. I revisit this in my commentary about Flutter below.

One of the advantages of being in Dart is that you have all of pub.dev at your disposal. Adding or removing dependencies is a breeze. 

Building for the Web was mostly seamless. I had one case where my code worked locally and failed in production, and I am not sure what the root cause was. I got around it by changing my implementation of randomized selection from a list. I should probably see if I can reproduce the situation in a smaller example and report the bug. Otherwise, though, it was very easy to keep the game running locally as I worked on it, in traditional Flutter Web fashion, as long as I wasn't changing assets or dependencies.

Challenges of Flame

Parts of the reference documentation are helpful, such as the list of effects, but there are other parts that seem to assume that the reader is already approaching the problem the right way. This is a bit hard to quantify, but I think what I was looking for is something more like the Flutter Cookbook: not a tutorial for beginners and not reference material for regulars, but something in between, that deals with common problems and their idiolectic solutions. In part, I fear that the Flame developers are hamstrung by the excellent documentation and examples provided by the Dart and Flutter teams. There is no way for a small project to keep up.

My vision for Fish Face was that there would be a magenta background with random shapes falling down the screen. This is the job of a particle system, but I could not make any headway with Flame's particle system; the best I got was a single particle. The documentation declares how robust their system of particle composition is, but I found the examples lacking for simple use cases. I spent a little time online looking for other tutorials, but I was running out of steam and this was low-priority juice. It was particularly frustrating because I knew how very easy it would have been to use Godot's particle system, to add it and interactively tweak it until it was good enough. This was a case where Flutter's code-based approach was much slower than turning a few knobs in a UI. Fortunately, Flutter is getting a widget preview system akin to Godot's "Run Current Scene" feature. I am heartily looking forward to how this will benefit my Flutter workflow. Thanks to the Discord denizen who pointed me toward this upcoming feature. (Incidentally, reading about Flame's particle system reminds me of the time I tried to learn the very basics of Niagara. I think there's a similar kind of shift in perspective that's required here. It's where I become tempted to do a deep dive and create a tutorial video to help others caught between worlds.)

When I sketched in the rough arrows of Fish Face's interface, my intention was that I could easily recolor them in-engine for animated effects. In Godot Engine, I would use a shader to do this, and I assumed Flame would make this as easy. Alas, the only mention of shaders in the Flame documentation says that they are coming later, once Flutter supports shaders for the Web. I don't understand why Godot Engine has shaders that run fine on the Web but Flutter and Flame don't, but it's probably because the Web is kind of an awful platform. I played with Flame's tint feature as a way to simulate what I wanted, but it was tinting not just my background, but the black outlines as well; that is, it tinted the whole image, not selectively the way I intended. I ended up just copying and pasting the images and manually coloring them in Gimp, then importing them as new assets, like a barbarian.

I enjoy programming in Dart and Flutter, but I still don't have a good handle on Flutter's animation systems. I have built a few demos but never really developed something that helps me internalize how to use implicit animations for juice. I have a sense that there's untapped potential there and that my ignorance is the impediment. Indeed, one of the things I'm hoping to tackle this summer is to make my Flutter-powered, TTRPG-inspired game more delightful by incorporating some UI animations. I realized too late into Fish Face that this exploration would be taking me in the wrong direction. In retrospect, I could have built the whole game just in Flutter, without Flame, and learned more of what I wanted to learn. There's a mismatch between the game design and what I was hoping to get out of the experience, but it's possible that I could only know this after having made this mistake.

Wrapping Up

It was a fun weekend with Flame, and I feel like I have a much better understanding now of when I would pick it up again—and it would not be for Fish Face 2: Umbrella Hat. Part of me wants to return to my Every Extend tech demo since that would be a much better use case for it. The Flame Jam is coming up later this month, but it's not a good time for me to another side project due to the end of semester and family obligations. Spending the weekend with Dart makes me eager to make some more progress with my Flutter-powered side project even though I still haven't played with the latest Godot Engine release. 

Wednesday, February 26, 2025

An Android Studio Live Template to simplify defining loggers in Flutter

This morning, I came across a discussion on the Flutter Forum about logging. I recently added rudimentary logging support to a side project, and I was interested to hear the perspectives of people with a lot more Flutter experience. Using Android Studio's Live Templates to reduce boilerplate sounded like a good idea. In my case, for every class where I want to log something, I want something like this:

final _logger = Logger('$MyWidget');

The dollar sign there is important because it uses string interpolation to insert the name of the class. If I rename the class, the refactoring tools in Android Studio will also replace this reference.

I don't remember the last time I made a Live Template for a JetBrains IDE, or if I ever have, so I decided I should take a not here. 

I created my Live Template in the "Dart" category. This is not necessary, but it is sensible. I called it "logger" and it looks like this:

final _logger = Logger('$$$CLASSNAME$');

Under "Edit Variables," I configured CLASSNAME to use the expression dartClassName(). Checking "Skip if defined" means that keyboard focus skips over the variable after expanding the template, which is a nice feature. I set the context to Dart/Other.

Using double-dollar-signs to escape a dollar sign is a little strange, but once I got past that, it was easy to set up. One unexpected pitfall is that if you erase a variable from the template and then retype it, you have to remap it to an expression: the IDE doesn't "remember" that the variable had a previous definition while working on the template body.

Monday, November 25, 2024

Walking away from a November game project: A reflection on NoGaDeMon 2024, Dart, Flutter, and Bloc

I would hate to make this a tradition, but it seems that I once again entered NoGaDeMon. National Game Design Month (NaGaDeMon) is November, and for several years, I created interesting little projects during the month. Last year, I was not able to pull a project together, and I'm afraid that's the case this year as well. However, I was able to learn a bit through the attempt, so I want to capture some of it here before it slips away.

Before November, I had been tinkering with an intersection of ideas related to posts in the last few months: interactive narrative games like my The Endless Storm of Dagger Mountain, which drew from the Powered by the Apocalypse tabletop RPG space, built around some concepts from Blades in the Dark and Scum & Villainy. I figured that, for November, I would try building a very small slice of the idea. For various reasons, I also wanted to try building and releasing a game using Dart and Flutter. I dug in and started making reasonable progress for a side project.

A few days into November, John Harper released Deep Cuts, a campaign and rules expansion for Blades in the Dark. I bought a copy and was quite surprised at the rules changes. I had expected little tweaks and balancing maneuvers, but Deep Cuts actually provides a complete overhaul of the most fundamental Blades action resolution system. This was too cool not to play with, so I rehashed my planned NaGaDeMon project, essentially starting from scratch to support some of the Deep Cuts ideas.

Before last week, I was able to get a very small version of the game working, letting the player experience a single, badly written game scene. The user-interface was just awful, so in order for the game to come together would have required adding a ton of content and a complete player experience design and implementation. Both of those would be tedious efforts, especially the latter, since I am not very fast with Flutter UI development. Part of the inspiration for choosing Flutter was to gain more practice with engaging UIs. 

About two weeks ago, the work of one of my committees exploded into taking most of my unassigned work hours, and this was not altogether unexpected. We also just got the good news that we will be hosting family for several days around Thanksgiving. This will be wonderful, although it also means these won't be hobby-project days. The result is that I've decided to put this project to rest. I did learn quite a bit going this far into the project, and that is the topic for the remainder of this post.

First of all, the obvious lesson is that if I wanted to really focus on learning to make a top-notch interactive Flutter UI, I should have chosen something with zero other design risks. I knew that the best I could do in one month was to make something just functional, yet I am not sure I was honest with myself about how ugly that would likely end up. Maybe I will find a game jam that will let me get a better handle on combining turn-based game timing with implicit animations.

Prior to November, I had been tinkering with some of these design inspirations in Godot Engine, which is of course the engine I used to build The Endless Storm of Dagger Mountain. I was using a rather conventional mutable-state object-oriented architecture. I found myself frequently frustrated by the lack of good refactoring tools for GDScript. This is a significant hindrance to evolving an appropriate design. This is part of what made me switch over to Dart, which is a joy to work with in part because of the excellent tooling support from Android Studio. 

A few summers ago, I spent a great deal of time studying Filip Hracek's egamebook repository. Nothing shippable came out of my efforts—I don't think I ever even blogged about it—but I did learn a lot. I was struck by how Hracek separated the layers of his architecture, and it was the first time I spent a lot of time in a game that used immutable data models. At the time, I had looked into the Bloc architecture and struggled to make sense out of it.

Approaching this November's project, I decided to dig deeper into Bloc. I spent a lot of time with the official tutorials and puzzling over this seemingly simple diagram:


The simple tutorials are simple, which is convenient, but the more robust ones separate the "data" component into a data provider and repository. It seemed clear that the game state could be conceived of as data, but I struggled to conceptualize where the game rules should live. The game rules can be considered part of the domain model, and as such, should be separated from the bloc. This would mean that a response from the domain model may be the modified game state, which then is echoed back through the bloc to the UI with a bloc state change. However, it's also reasonable to conceive of the game state itself as the data layer and the "business logic" as being the transformations of that state. Indeed, this seems to be the difference between the simple and more complex tutorials: the simple ones deal with simple in-memory state, and the more complex ones draw data from different sources and transform them in the data layer. 

Of course, there is no silver bullet. Given the tight time constraints on the project, I simply considered the immutable game state to be my data layer, and I put the game logic in a bloc. I also simply passed the game state along to the UI, but in a more robust solution, I would have had clearer separation between layers. Including a dependency between the UI and the data layers was a matter of expedience and the intentional incurring of technical debt.

My first pass at the implementation had me writing my game states and bloc states by hand. The Equatable package meant that I didn't have to fret over writing some of the boilerplate that's necessary to do state comparisons, and it was easy to integrate this in Android Studio using Felix Angelov's Bloc plugin. When scouring the Web for help with Bloc, one quickly also comes across discussions of Freezed, which library is also integrated into Angelov's plugin. I had tinkered with Freezed in my egamebook-inspired explorations, but I have not shipped anything that uses it. After having built up my understanding of Bloc using Equatable, Freezed was an obvious next step. Next time, I would jump right into using it for cases like this.

Writing a functional Flutter user-interface was straightforward using BlocBuilder. I found this to be a convenient way to conceptualize the game, especially since it had very clear states. For example, in my original explorations (before Deep Cuts), I had the player choosing an action from a list, then customizing the action with various options from Blades in the Dark, such as pushing yourself to trade stress for dice. After rolling the dice, the player is now in a different state of the game in which they are responding to the result, such as by resisting its consequences. This was elegant to express in the code, and I am confident that with enough effort, I could make a compelling user experience out of it. By contrast, Dagger Mountain used an architecture inspired by MVP but that depended too heavily on the undocumented, unenforceable behavior of coroutines. Both of these are "only jam projects," but they are helping me to conceive of how I would approach something more significant in this problem domain. The aforementioned coroutines were my solution to synchronizing the model and view states (for example, to finish an animation before continuing to the next step of the narrative); I'm fairly certain I understand how I can do that with bloc's events and states, but since the November project will remain unfinished, there is risk.

All this exploratory coding meant that I did not follow a test-driven process. I ended up not getting into the testing libraries specifically for bloc. It's possible that this would have helped me better to conceptualize the business logic versus the domain layer, but that remains future work. 

There are still a lot of questions about the game design itself. Indeed, this entire exploration is inspired by design questions around the adaptation of Blades in the Dark tabletop gameplay into a digital experience. Citizen Sleeper is the only project I know of that has worked in this space, and it's a fantastic interpretation. I only became aware of Citizen Sleeper after I started doodling my own ideas, and it's interesting to see where they converge and where they diverge. I hope to dive back into this design space later, but for now, my attention must go toward wrapping up this semester, planning for next semester, and enjoying the upcoming Thanksgiving break.

Monday, April 17, 2023

Configuring a Flutter Web App for Continuous Integration using GitHub Pages

GitHub Actions lets you build and deploy a Flutter Web app directly onto GitHub Pages. However, if you do this in the naïve way, it won't work properly because the base href for the app will be wrong. You need to give the repository name as the base href in order for the built site to work. Here's a configuration file that will work out of the box.

name: Continuous Integration

on:
  workflow_dispatch:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: subosito/flutter-action@v2
        with:
          channel: 'stable'

      - name: Download Dependencies
        run: flutter pub get

      - name: Run Unit Tests
        run: flutter test

      - name: Build
        run: flutter build web --base-href='/${{github.event.repository.name}}/'

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./build/web
          force_orphan: true
          user_name: 'github-ci[bot]'
          user_email: 'github-actions[bot]@users.noreply.github.com'
          commit_message: 'Publish to gh-pages'

The workflow-dispatch is optional. It gives you the option to click a button on the Actions page of your repository to force a build rather than waiting for a push. 

Saturday, December 24, 2022

Flutter, Godot, Plans, and Regrets

A conversation last summer put a bug in my head for a game design. It's been flitting around for months, and I feel like the idea could be fun for a niche audience. In fact, it's the same idea I wrote about last July, when I tried some exercises from Justin Gary's book. Two or three weeks ago, I got out my paper prototyping supplies and whipped up a minimally playable version. Although the paper version was anemic, it showed that the core ideas could be fun. My intention though had always been for it to be a digital experience, and I knew that a lot of the experimenting would require trying different content and interactions in digital form. 

This led to me to explore two different options for digital prototyping. My first option was Flutter, which I continue to be fascinated by. I have used it for several side projects and a few classes, and I am particularly interested in the elegance of Dart and the separation of UI and model-layer concerns. My other option is, of course, Godot Engine, which I have used for most of my game development the past several years. A significant benefit of Godot Engine over Flutter is that I just have a lot of it in my head right now, so I don't have to spend as much time scouring documentation for just the right thing.

The game design itself is, fundamentally, a single-player card game. At this point, I've made several different prototypes in both Flutter and Godot Engine. Indeed, I've made so many, with enough variations on their directory names, that I've had to set up a separate directory just for abandoned prototypes. What I've come to realize just recently, though, is that I have spent a lot of time on the wrong things. To be honest, it's not clear if these things are wrong since I did learn what I wanted to learn from them, but on the other hand, they are wrong because, six months and many hours of labor later, I don't have a version of the game that I can send to my friends to playtest.

In particular, I've been having serial headaches with a fundamental card game interaction problem: I want to drag cards from one area (e.g. a hand of cards) to another. I want this to feel good to the player, so the cards should animate nicely into position. If they are over an area where they make sense to play, the player should get some feedback. If they are dropped in an illegal area, they should go back to where they came from. 

Note that none of these issues are core mechanisms: they are UI design problems. They are important UI design problems indeed, and one of my concerns going into the project was that, if I was going to sink personal time into it, I wanted to invest in the right tool stack so I would have to do minimal re-engineering. However, reading Lemarchand's A Playful Production Process—which I am doing in preparation to teach CS390 next semester (blog post coming soon)—got me thinking that I am trying to solve a production problem while I should be focusing on preproduction problems. That is, my goal right now should be to determine if the game should be pursued at all, not whether it should feel good. This means making a toy, a playful artifact, rather than a vertical slice.

Rather than regale you with the chronological narrative of my discoveries, I will summarize some of what I found for each of my engine options.

Flutter:

  • The dev tool support here is incredible, with easy refactoring and convenient language constructs.
  • The Draggable and DropTarget widgets are really nice for most drag-and-drop purposes, but not for my case. When dragging a widget, a separate element is made for the version that is following the mouse, and it is irrevocably lost on release. This makes it impossible (or at least impracticable) for handling my case where a card animates smoothly into position or returns to the player's hand. 
  • A custom drag-and-drop system can be built out of Stack, using Positioned widgets. That is, rather than nesting widgets in the normal Flutter way, one can get complete control over an entire canvas of widgets. AnimatedPositioned makes animating these into position a breeze. Indeed, I was pleasantly surprised at how easy this was to express things like, "Keep all my cards in my hand evenly distributed across this widget." The problem is that, when recognizing the drag gesture, the mouse position is consumed by the widget on top: one cannot get mouse entry/exit events for the widgets underneath. 
  • It seems like, at this point, I'm just building my own UI framework and may as well just be using Flame. The trouble is that then there's yet another piece that I don't know, and development is slow and awkward for me.
As always, if any of you readers knows more about Flutter than I do, please feel free to correct any of these misconceptions.

Now, Godot Engine:
  • Of course, Godot Engine has the benefit of its visual editor for building scenes, including composing them. This makes it very easy to break up a project into pieces that are individually, but manually, tested.
  • Godot Control nodes do have drag-and-drop support, which I had not come across before. Like Flutter, you make a separate view node that is visible just while dragging. It would be great for normal drag-and-drop interfaces, but not for something like a card game. 
  • Which controls receive mouse focus is again a problem, but it's solved by keeping track of all the geometries in a "tableau" class. It watches for the mouse and, at the level of the table, keeps track of where the mouse is, which areas should be highlighted, when cards should be animated, and so on.
  • One of the things I built in a Godot prototype was an EventQueue to manage some of the in-game events, such as waiting to start a round until an animation was finished. That was something I found particularly awkward to do in Flutter. None of my past projects used an EventQueue, and I suspect that if I finish this project, it will need something like that. 
  • Refactoring in Godot Engine is a huge hassle. It is easy to build a thing. It is almost impossible to rebuild a thing differently.
How much of this really matters right now? I'm not sure. It's Christmas Eve, and I want to spend most of the next several days with family and friends. I'll have some time to dive into code if I want to. I still have significant prep to do for this new class I'm teaching in Spring, and of my other two classes, one is almost ready to go, and the other needs maybe a half day more of work. If I do dive into code, I think I will have to make yet another new project though, and I will have to hold myself accountable to making something simply playable. I need to stop myself from getting distracted by these interesting problems of software architecture and player experience so that I can determine better whether this game is fundamentally worth pursuing or not. I suspect this means I will turn back to Flutter, where I can easily refactor and whip up UIs that respond to model-level changes.

Merry Christmas!

Friday, December 9, 2022

What we learned in CS222 (Fall 2022 edition)

Regular readers will know what time it is: it's time for the next installment of What We Learned in CS222. As has been my recent custom, I moved this exercise from the final exam timeslot into the penultimate day of class, the final day of class being reserved for students' final presentations. We only had seven students in class this morning, which is less than half the enrollment but not far from what I've been getting the last several weeks.

I had them start by quickly making mind maps on the theme of programming. I only gave them three minutes to sketch them out, telling them afterward that it would be a great study exercise to compare your current map to the one made on the first day of class. The exercise was really to just prime the pump for them to answer the question, "What did you learn this semester?"

Here are the 71 responses, sorted by the number of votes earned at the end of the process. Each student had four votes, but either someone misunderstood the instruction or I lost a sticker in transit.

  • Clean Code (7)
  • TDD (6)
  • GitHub (3)
  • Code Review (3)
  • Agile fundamentals (1)
  • Version control (1)
  • User stories (1)
  • Model-View Separation (1)
  • Flutter + Dart (1)
  • Naming (1)
  • Requirements analysis (1)
  • Software architecture (1)
  • SRP
  • Acceptance testing
  • CRC cards
  • CI/CD
  • Pair programming
  • Mob programming
  • Using APIs
  • Using Android Studio / IDEs
  • Forking repositories
  • Feature branching
  • Committing in git
  • Commit messages
  • Pushing in git
  • Pulling in git
  • Licensing
  • API keys
  • Defensive programming
  • README.md
  • Pubspec.yaml
  • Achievements system
  • Pitching project ideas
  • Incremental development
  • Sprint
  • Project demos
  • Iterations
  • Breaking down epic stories
  • Emulators
  • Red-Green-Refactor
  • Comments
  • Test data files
  • Functions (especially parameters, in a Clean way)
  • Null safety
  • Async/Await
  • Stateful & Stateless widgets
  • DRY
  • gitignore
  • Reading package documentation
  • Using outside resources
  • Local saves
  • Save/Load features
  • Export/Import features
  • git commands
  • Dependencies
  • Project time management
  • File names
  • Simple, clear class names
  • Encapsulation
  • Software craftsmanship
  • Making UI buttons
  • Code side-effects
  • Delegating (in Software)
  • Delegating tasks
  • Wrappers
  • Polymorphism
  • Defensive copies
  • Text wrapping in a UI
  • Text wrapping in code
  • Test coverage
  • Non-programming skills

There are a few interesting things in this list. I was surprised that there was relatively little about Flutter and Dart in particular. I was glad to see it show up as a concept itself but more excited to see that a student recognized how very cool async and await are. Breaking down epic stories is important since just yesterday we did an in-class exercise where no one remembered that conditions of satisfaction are part of user stories, despite this being a recurring topic of the semester. Specific Clean Code ideas seem to be less represented here than in past years, more emphasis here being placed on technological issues of version control. Unfortunately, there is nothing funny in the list.

We found it easy to draw a line in the list and see that the top four items were selected by the community as the most important. I explained to them that these four will feature on the final exam, and it's up to them whether they want to share this information with their missing teammates or not. This elicited the expected nervous laughter.

Thursday, August 18, 2022

Summer Course Revisions: CS222 Advanced Programming

It has been a little wobbly here due to my teaching schedule changing, as discussed in my previous post. Because one of my courses didn't make enrollment, it looked like I was on deck to teach my department's CS200 Computers and Society course, which is a university Tier 2 core curriculum course in the natural sciences domain. I've never taught it, but it sounded like it could be an interesting challenge. I started doing some prep work on it, and then I was notified that a colleague was willing to swap a section of CS222 with CS200. CS222 is a course I designed, teach regularly, and love to teach. Also, that CS200 course was scheduled right before my other two back-to-back courses, and so it would have had me teaching continuously from 12:30 to 5:00 on Tuesdays and Thursdays. Thanks to the generosity of my colleague, I am now scheduled to teach CS222 MWF instead of that CS200 class, which is one that he has prepped and taught regularly.

This all happened two days ago. Now, the easy thing to do would be to just copy over my Spring CS222 plans, change some dates, and be ready to go. This is not my way. Regular readers may recall that I ended my reflection on Spring's section by putting forward the proposal of changing the technology that I use to teach CS222. This has been on my mind intermittently over the summer, especially as I spent several weeks exploring game design ideas in Flutter. Pushing aside lingering doubts, I began to design the course with this new technology foundation, and today, I published my plan for the first several weeks.

Obviously, the most important transition has to do with getting the students up to speed with Flutter and Dart. The truth is that they have come in with fairly weak understanding of Java anyway, and so rebooting their language learning experience may have a net benefit. The documentation at flutter.dev is excellent, and you can see on the course plan that I am having them complete several standard Flutter and Dart tutorials and codelabs.

The most important factor here is time. Knowing that they will all have to be going through these beginners' steps means that I am accounting for it in my expected weekly commitment. A few things that I had them do in Java have been pulled out, such as the GradeTool refactoring exercise. One thing that really excites me is that students will be running into higher-order functions and stream processing earlier and more consistently. It is to Dart's benefit that it doesn't carry all the baggage that Java does. I was excited to find the iterable collections codelab as a great example for helping novice programmers understand how this works.

I have started a sample solution to my traditional two-week project assignment and, based on that, traced backward what kinds of things I need to introduce in class in the first three weeks. That is, I have developed a running example in class that, I think, will prepare them to build up a version of the two-week project that is essentially the same as what my students have done with Java and JavaFX in the past. 

I do not expect their reading of Clean Code to be significantly hampered by changing the language of teaching. The examples in the book are all in Java, but I don't think the students ever spent a lot of time with the examples anyway. The prose mostly stands on its own. Indeed, in my ten or so years teaching CS222, I don't remember a student ever referencing or asking about a code example in the book.

Today, I recorded and published my first tutorial video based on the stack of Android Studio, Flutter, and git. I hope to make a few more tutorial videos during the semester based on the kinds of questions my students have or difficulties they encounter. 

I'll be sure to write more about this transition in the future, certainly in my end-of-semester reflection if not before. In the meantime, if you have questions, comments, or suggestions, please feel free to share them in the comments or to email me.

Thursday, July 8, 2021

Refactoring the Thunderstone Quest Randomizer: Generating dart code with source_gen and build_runner

This post is part of a series about the implementation of my Thunderstone Quest randomizer. For more background, see my original post from Summer 2020 or my earlier update in Summer 2021. 

I remember when I first learned to use SharedPreferences to implement user settings. It was exciting to see the feature work in the Web build. I remember thinking that the implementation was a little messy, but that I could clean it up as I go. Predictably, in the excitement of adding new features, I did a lot of copy-paste coding, which led to bloat in the SettingsModel class. Some days ago, I decided to dive back into that implementation and clean it up.

If you look at the previous implementation of SettingsModel, you will see that the logic for managing user preferences is spread throughout the file. Each preference needed to add a key constant, a declaration, an accessor and a mutator, and clauses to the _loadPrefs, clear, and _updatePrefs methods. Adding the second and third ones were easy, but the more I added, the more cumbersome it became. SettingsModel also serves as a facade, hiding the fact that some preferences are handled quite differently than others.

My initial attempts at refactoring this had me spinning my wheels. Clearly, I could extract the behavior of a preference into its own class; this would just be a matter of traditional OOP. It would make sense for each of these to be a ChangeNotifier. (Check the docs for more about this important Dart class.) The SettingsModel itself could still be a ChangeNotifier that simply echoes any notification from the preference objects it contains. To set this up, I needed a way to iterate through all of the preferences. A list of preferences would do the trick, but I also needed independent fields for each preference. To reduce duplication, then, I wanted an automated way to loop through all of the Preference fields in the SettingsModel.

I assumed there would be a way to do this with runtime reflection, but I had no luck with this. I found a few references to dart:mirrors, but this does not seem to be supported in Flutter. Every time I tried to get reflection going via reflectable, I ran into a rat's nest of dependencies that I didn't understand—notably build_runner. It seems to me that, for the high quality of Flutter documentation, there was almost nothing about solving the problem I was facing.

I found a comment on a defect discussion that piqued my interest: it was a claim that anything you would want to do with reflection would instead be more idiomatically done with code generation. After several failed attempts with reflection, I started trying to better understand how to use source_gen for code generation. This video by Creative Bracket helped clarify how the pieces fit together, especially the relationship with the aforementioned build_runner.

This helped me to build a prototype that, at build time, scanned SettingsModel and added to it a list of all the preferences declared within. This was the move that gave momentum to this approach. I was able to enhance this source_gen-based approach first by generating accessors and mutators for each preference based on its declaration, and then to generalize these along with a preference type variable. In the end, this approach required no changes to the client code, which was an added bonus. 

I extracted the preferences API into its own library. It contains an abstract Preference<T> class with several implementors, including common types such as BoolPreference and IntPreference as well as special-case cases such as BrightnessPreference. Each new preference type is very simple, only having to declare the methods that are used for reading and writing to the SharedSettings object.

I have debated whether SettingsModel should be a facade or whether it should return references to the preference objects, which would allow parts of the UI to consume only those preferences that it cares about. This would lead to an awkward proliferation of producers rather than the convenient wrapping currently in SettingsModel.

The implementation is up on GitHub and licensed under GPL v3.0, so feel free to take a look if you're interested in more details.

Tuesday, June 22, 2021

Summer Flutter Development: Internationalizing the Thunderstone Quest Randomizer

Last week, I started laying down some of my Summer projects and began putting together course plans for Fall. I expected that, on Monday, I would jump in with both feet and get those new course sites up and ready. However, on Sunday, an unexpected thing happened on the GitHub page for my Thunderstone Quest Randomizer: a stranger posted a feature request to localize the app to French. 

I first wrote about this app about a year ago, when I described the reason for implementing a new randomizer in Flutter. Since then, I've released the odd improvement, including a significant internal improvement at the beginning of summer to allow the app to be built using null safety. That effort reignited my interest in the game, and so we've been playing a lot of it since then.

Sunday's request for localization support intrigued me for a few reasons. First, it showed that someone besides me actually looked at the open source code and cared that it was hosted up on GitHub. Second, the person who made the request is involved in proof-reading the actual French translation of the game, and it's neat to have "insiders" taking a look at a hobby project. Third, I talk about internationalization a bit in my classes, but I have never actually tried to internationalize a nontrivial application before. A quick look at Flutter's internationalization documentation intrigued me enough to make me want to get my hands dirty.

There were two major problems to solve regarding internationalizing the app: the app interface and the card database. It was pretty clear from early in my analysis that I could not easily use the same approach for both. I will address each in turn.

Internationalizing the app interface was mostly a matter of following the Flutter documentation. I brought in the Intl package and did the requisite configuration. I extracted all the user-facing strings into an application resource bundle file, which is a format I had not seen before. Authoring this JSON file involved a lot of tedious typing, and though I was on the verge of looking for tool support, I ended up just powering through it. I wrote descriptions for each string key, and I documented the parameters for each template string (which they call "placeholder resources"). Something that was not obvious to me from reading the documentation was that, while the intl tool converts regular string resources into dart literals, it converts placeholder resources into functions. This is quite convenient but it was not what I expected.

I used Visual Studio Code for all of my development, and it's worth noting that there's a slight hiccup in the toolchain when building internationalized applications this way. The arb file is transformed into dart code during the build process, but that build process is not triggered by saving the arb file. Writing UI code that uses the generated dart files then will give false-negative compiler errors until the whole application is reloaded. Fortunately, this can be done with the hot-reload tool. It's a workflow I got used to, but it seems like it would be convenient to hook the arb file's saving into a regeneration of the dart code.

Adding localization support to the card database was a more interesting technical challenge. The cards are all specified in a large YAML file that is loaded during application initialization. My original plan was to load only the localized names into memory, which would work fine if one only ever wanted the cards in their platform's default locale. However, when I got into it, I realized that I needed an easier way to change languages for testing. I had assumed there would be a toggle in Chrome that would simply change locale, but all the documentation I found said that it requires restarting the browser as well. That's too fiddly. I also ran into a synchonrization problem: the rest of the application assumed that the YAML would be parsed only once, but if changing the language required re-parsing, then the app architecture needed adjustment to account for the time this takes. 

After having written an almost complete solution using this approach, I abandoned it in favor of a  simpler one. Now, the YAML file is parsed only once, but all the localized strings are stored in memory in simple string maps. This certainly uses more memory than the alternative approach, but it is much easier to swap languages: using Flutter's simple state management tools of Providers and Consumers, the UI can watch for the language key to change and then alter which localized text is shown on the screen. I have not done any profiling to see what the actual memory costs are, and so I'm just banking on contemporary environments having memory to spare.

As of this writing, the app does not have French language support exposed because I only have the two resources localized that the original GitHub feature request gave as examples. However, it is set up so that uncommenting a single line in the settings view will expose language selection options. When someone wishes to localize the data into any language, then, it should be an easy matter to incorporate it into the app.

It was fun to spend a few days learning about Flutter's internationalization support and adding that to my app, even if right now, there's no difference to the user. I learned a few new tricks, and importantly, I gained some experience that I will be able to share with my students in the future. No more hand-waving about internationalization: I can show them an example of exactly how I've done it.

Friday, July 24, 2020

Something like a Summer Devlog #5: Fluttering back to Thunderstone Quest

TL;DR: New Thunderstone Quest Randomizer with combo support. Check it out at https://doctor-g.github.io/ThunderstoneQuestRandomizer.

Background and Inspiration


By all accounts, the plan for this week was to work on a plan for transitioning the third of my three Fall classes to its online mode. I had done some of the prep work required and started conversations with colleagues about it. Then I received my New Horizons expansion for Thunderstone Quest. Normally, this wouldn't derail a whole week, but I was excited to add the new cards to my Polymer-powered randomizer app, TQR, which is online here and which I wrote about here. I was pleasantly surprised how easy it was to add the new cards: just dropping them into the JSON configuration file let the rest of the engine find them and incorporate them into the user experience. I rarely maintain my old code, but it was a good feeling to have something I wrote in May 2019 hold up.

...well, except for one detail. The expansion added a new kind of Market card: Allies. The expansion rulebook suggests adding the ally randomizer cards to all the rest of the market randomizers, and then adding the Ally cards to the "Any" spaces in the market when they come up. This struck me as curious when I read it originally, since I never thought to mix all my market randomizers together: I keep them separated by type (Item, Weapon, Spell). Indeed, this kind of separation is encouraged by the physical dividers provided in the base set. More relevant to this story, the TSQ implementation assumes that you figure out the distribution of Item, Weapon, and Spell first, then fill the market. That is, since there were always three of two types and two of the other, it determined that first, and then drew from the corresponding decks. There was no way for me to add Allies without gutting this part of the randomization, which was also tightly coupled to the whole user experience.

I began to imagine what it would be like to reimplement the randomizer in Flutter. I taught myself enough Flutter this summer to be able to include it in my HCI course plans, and I developed a sample solution to a two-week project for that course. It was, of course, a simple project, and I had in the back of my head that I should do something more significant. Why not a new Thunderstone Quest randomizer? This would give me the opportunity to get a better grip on Flutter, plus I could add one of the key features that didn't make it into the original implementation: combos. I have seen various Thunderstone communities reminisce about a particular randomizer app from Thunderstone Quest's predecessor, Thunderstone Advance, for its support of card combos. My mind idly went through a couple of potential implementations for combos, and I convinced myself (without building any technical demos or writing the actual requirements) that I could get a version of this working. That was enough for me: Monday morning, I was off to the races and working on a new Flutter-powered randomizer.

A point of clarification: My old randomizer is officially named "Thunderstone Quest Randomizer" and abbreviated TSQ. After some failed attempts to come up with a catchier name for my new implementation, I also named it "Thunderstone Quest Randomizer." When it's not clear which one I might mean from context, I will refer to the old one by the abbreviation and the new one by the full name. At least, I will do this until someone comes up with a better name for it, or until I get really excited about "Thunderpants" as a memorable name. Lion-O would dig it.

Data Wrangling


One of the first things I did was transform my TSQ JSON data file into YAML. It wasn't too bad to add the new data to JSON, but there is a lot of cruft. I happened to be recently back into using YAML, which I traditionally like better than JSON, but the prevalence of JSON has me using it more often. More recently, I've been tinkering with GitHub workflows, which are specified with YAML, and it reminded me how much I prefer it for structured data. I used an online translator that was fairly painless and found a convenient API for parsing YAML in Dart (the programming language of Flutter). Adding new libraries to a Flutter project is as simple as it should be in a modern programming environment: drop a line into the awkwardly named pubspec file, which is itself YAML, and you're off to the races.

I wrote most of the parser using TDD, and I found the testing environment to be similar to ones I've used for Javascript: easy grouping and idiomatic matchers that benefit from higher-order functions. I decided from the get-go that I would not make a fundamental architectural mistake that plagued TSQ, which was the very Webby assumption that the JSON data should be used directly by the view. Instead, I added a semantic model layer in between, so that in the new randomizer, the YAML file is parsed into a list of Quest objects, each of which has Heroes, Monsters, Items, and so on. I didn't see a way to automate the population of the domain model as I would do with a library like GSON, but it wasn't much code to grab the data I needed from the YAML parser. Subscript notation for random map access also meant this was very short to write, compared to Java, for example.

This was also a place where I learned to be a bit more idiomatic with Dart. I originally had repeated code of this form:

if (node['Hero'] != null) {
  for (var entry in node['Hero']) {
    // Parse hero
  }
}

This sort of defensive programming was required because attempting to access a key that doesn't exist, in the YAML library implementation, produces an exception, not null as I might expect. After reading some documentation on best practices for Dart, I came across the curious "??" operator, which I have not seen in any language before. It allowed me to change the code above into something like this:

for (var entry in node['Hero'] ?? List()) {
  // Parse hero
}

This means that if node['Hero'] is null, then use an empty list as the value instead, which of course terminates the iteration right away. It's much more terse and expressive: less imperative cruft. I will mention, though, that throughout the implementation, I was nagged by a feeling like there were better, idiomatic ways to do things, and that if I had a code review by a Flutter regular, they could point out where I might grow next. I am sure I can help my students in the Fall go from Novice to Advanced Beginners, but the gap between Advanced Beginner and Competent is foggy.

Let me dig into that just a little bit more to show what I mean. Here's an excerpt from the 1.0.0 release:

for (var entry in node['Heroes'] ?? empty) {
  Hero hero = new Hero();
  _parseCard(entry, hero);
  _quest.heroes.add(hero);
}

for (var entry in node['Items'] ?? empty) {
  Item item = new Item();
  _parseCard(entry, item);
  _quest.items.add(item);
}

There are eight blocks basically like this for the eight types of cards. The guts of the parsing is the same for each card, so it was easy to extract a function that. However, the actual runtime type of each card is different, and they get added to different lists within the Quest. It feels like a terrible repetition to have almost the same code eight times, but it's not obvious to me how I could extract this to have fewer lines of code, especially knowing that Flutter does not allow for runtime reflection. I could put the parsing logic into the domain model classes, but that pollutes the model with data concerns, and making separate per-class parsers for each would add more code than it removes. If this looks like a fun refactoring challenge to you, I welcome you to open a pull request!

Combos


The combo system that I implemented operates on a matching of two kinds of data. Each of the keywords on the cards (like "Humanoid" or "Edged") are encoded in the YAML file and stored in the domain model. Additionally, each card that references these data on other cards has encoded a combo list. For example, Stormhand has a bonus for Edged weapons, so his combo list includes the term "Edged". When choosing a card, then, the combo can arise in two ways: either the current card has a keyword that is in the combo list of a card on the tableau, or the current tableau has a keyword that is in the combo list of the current card. I introduced a combo bias to the application, which is a probability that a card would be filtered out for not being a combo.

It turns out that this is not entirely sufficient, since not all powers trigger off of keywords. Some trigger off of other properties, such as the presence of diseased wounds or certain words in the card title. To handle this, I added a "meta" category to the card database. These are folded into the keywords when searching for combos. For example, Blizzard gets more powerful the more frozen wounds you have, so this card gets "Frozen" in its combo list, while cards that give frozen wounds, like the Tundra Wolf Pack, have "Frozen" in their meta.

In human language, it works a bit like this. Choose a random card to potentially add to the tableau. Roll the dice to determine if we're looking for a combo. If so, check if there is a combo, and if so, keep the card; if we're not looking for a combo, just keep the random card. Then, see if the card can actually fit in the tableau (e.g. the Marketplace is not full).

This is why I called it a combo bias and not a combo chance, because finding a combo does not mean that the card will be used: it's more of a probability of applying a high-pass filter. The app currently allows the user to set the combo bias between 0% and 95%. In casual testing, I've never had a case fail to match or take an observable amount of time except in cases where I specifically chose quests that have no combos, such as using just the Bandits of Black Rock quest, which does not have enough cards to fill a tableau.

There are parts of the implementation that are woefully unoptimized, such as the on-demand querying of which keywords are on the tableau. I expected this to run much more poorly than it did, but the truth is that because the N is so low, and computers are so fast, it made no observable different to performance whether I cached the data or created and merged new Set objects each time.

The sequence of card selection is important for combos, of course, and I tinkered with this a bit throughout development. As of this writing, the sequence is Heroes, Guardian, Dungeon, Marketplace, and Monsters. We did not encode any combo data for the Dungeon tiles, since they generally key off of things that are always in play, like the four classes, light, and gear tokens. This means that the hero selection is arbitrary, subject to the selected strategy, but anything else except Dungeon Rooms down the chain can be part of a combo. I think this is probably a good configuration, but I'd be happy to hear other perspectives on this.

State Management


Naturally, I had some frustration with Flutter as I tried to wrap my head around it. The little sample project I mentioned before existed on one screen with a single stateful widget, but for this project, I knew I had to handle state more carefully. The documentation gives a few pointers and then says, essentially, do whatever you want. It's nice to have that freedom if you have time and mentoring. I decided upon the approach recommended for simple applications, which was to use a ChangeNotifier to propagate changes to the user settings through the rest of the application. Near the end of development yesterday, I got to wondering if I had correctly discerned the difference between using the Consumer and Producer objects, and I may yet go revisit that. In the meantime, though, the basic architecture of the application is that the randomizer screen is its own stateful widget which is the generated tableau, and the options in the Settings page are shared via a ChangeNotifier.

The SettingsModel and its corresponding screen are another area where I see a lot of repeated code, but it's not obvious to me how to use the tools of the language to factor out the duplication. I can handle cases like showing one checkbox per quest to have it selected or not, but it's less clear to me how to handle the suite of checkboxes around the application appearance.

Implementing different hero selection strategies was done with the Strategy design pattern, of course. I was happy to be able to implement this pattern in a very direct way, and it worked as expected. It did result in a strange separation of concerns, though, where the thing called "Randomizer" delegates to a strategy that is, for convenience, defined alongside the SettingsModel. This is another area of code that is a bit gross, could benefit from refactoring, but also works pretty well if you just leave it alone.

Flutter is famous for its support of contemporary user experience design, including ample animations. I knew I wanted to add something along these lines, although going into the app I didn't have a concrete notion of what it would be. As I got into the implementation, I decided to keep it simple: use fade transitions to show when a new tableau is generated. Note that the animation between the main randomizer page and the settings page was essentially "free" since it is built into the default app routing behavior.

Animation


It took me many, many efforts to get the animation working properly. The documentation made it seem like FadeTransition should be easy to put into play, but I tried that, several other things, and then came back to that. After hours of hammering at it, I finally had a sense of how the animation worked, and this revealed to me that I had deeper problems with how I was conceiving of the animation. When I stopped to consider what was scratching at the back of my head, I realized that I had to separate and synchronize the notions of fade out, generate tableau, and fade in. It seemed to me that I should be able to do this with a chain of futures, but the animation framework seems to only provide listeners, not futures. The result is a bit of confused code in the main randomize screen: the logic for the sequence of events is not laid out sequentially in code, but spread across a few different functions to handle different cases. I don't like this, and it's not clear to me if it's a property of the framework or a limitation to my understanding.

Another piece that caused me minor trouble, but is worth mentioning, is the asymmetry in how animation events are reported. When you play an animation forward (fade in), it ends with a "completed" event. That makes sense, so I assumed then I could listen for the same thing when playing an animation backward (fade out). It doesn't, however: when an animation is finished playing backward, it fires a "dismissed" event. Dismissed? I had seen this in the documentation assumed it meant, you know, that the animation was dismissed. I would never say that a reversed animation was dismissed, and this nomenclature surely doesn't show up in any of the game engine's I have used, where reversing animations is commonplace. Heck, I can't even find in the Flutter documentation now the definition that this is what they mean by "dismissed." It's a really odd choice to me.

In the end, though, I got the fade animation working. Originally, I had some idle thoughts about card selections flying in from the side, but really, I think the fade transition fills the bill.

Tool Notes


Let me say for the record that flutter doctor is amazing. Indeed, the whole flutter command-line tool is a master class in how to conveniently manage a development environment. It was frustrating, then, that there were a few cases where Visual Studio Code did not behave as I liked. I regularly had to kill and restart debugging sessions. This would not be such a frustration except that I had to do all my development in Windows because of a problem with hot-reload on Linux. I was using Windows because Linux wasn't providing me with hot-reload, but the hot-reload frequently failed on Windows, so what then? Also, I had bad behavior from the debugging sessions when changing my assets, but it wasn't clear if that's an expected behavior or not.

The biggest frustration about Visual Studio Code, though, was that for the life of me I could not figure out how to tell it to run all my unit tests. I could easily choose a suite of tests to run but not the whole directory of tests. Dropping into the shell, flutter test did the trick, and I ended up regularly just doing that and, when failures were reported, going back into the IDE to run the suite with the failure so I could get the more readable error report.

It took me more than halfway through development to realize the power of the Ctrl-. shortcut, but once I found it, it had a dramatic improvement on my development speed. This shortcut brought up a menu of options that I hadn't even thought to hope for, the real beauties being "Wrap with Widget" and "Remove Widget". Flutter code can end up with lots of nested widgets, which yes, really ought to be extracted into more manageable chunks. Be that as it may, manually wrapping or removing widgets is a painful process of braces-matching. These shortcuts were brilliant time-savers.

Also, the GitHub workflow I wrote about the other day to automatically publish on GitHub pages worked like a charm.

Final thoughts


I have dubbed this release 1.0.0 and posted about it on BoardGameGeek, where it's already generated a few thumbs-up and positive comments. I will be adding a link to this blog post there too, for anyone who wants to read the long form commentary. I would be remiss not to include my gratitude to my eldest son, who helped with encoding the combo data and keywords into YAML. He did a great job, learning the notation and the ideas quickly.

Awkwardly, my own set of Thunderstone Quest is not currently in a state to use a randomized tableau! Before receiving the expansion, we have been playing the Epic variant, so I have several cards pulled from their stacks for this purpose. Maybe after we play the authored quests that came in the expansion, I can excite my kids to do a bit of sorting so that we can play some games with different combo biases.

Right now, the fact that a given card was part of a combo is not exposed in the user interface. You can actually see them logged if you open up the console: for example, on Chrome, press F12 to open up the developer tools, and you can see combo card selection reported there. I've thought about making this an option in the UI, but this goes into the "that's enough for this week" category. Maybe I should open an Issue about it on GitHub so I don't forget.

I have not taken a careful look at internationalization nor accessibility of the new randomizer, but these are significant topics that I would like to investigate. I want to include at least some coverage of both in the Fall HCI class, and one of the reasons for making a bigger Flutter app was to motivate the desire for such things. I don't think I will do it right away, though. I worked on Thunderstone Quest Randomizer more-than-fulltime since Monday, and today I need to step back from it and do some family painting.

Thanks for reading. I hope you enjoyed the commentary and that you find some utility in the Thunderstone Quest Randomizer. It's free software, so please feel free to use it, share it, study it, and modify it to your heart's content.

Wednesday, July 8, 2020

Deploying Flutter Web apps to GitHub Pages

UPDATE: This post presents what I consider a slightly easier approach.

I spent most of the day trying to find the easiest way for my students to publish Flutter Web apps to GitHub Pages. My quest started at zonble's Medium post, proceeded with my taking a closer look at GitHub's workflow and actions documentation, and finally tripping across the handy GitHub Pages action. After a bunch of testing, I'm feeling confident about my approach, which I will explain below.

The essence of the approach is to use a custom workflow by placing the following file in your project in the path .github/workflows/workflow.yml. Then, whenever you push to master, the workflow will be triggered and your built application will be deployed to GitHub Pages.

This approach is susceptible to the known problem with first GitHub Pages deployments using GITHUB_TOKEN. The problem can be entirely avoided by creating and then specifying a personal access token for the repository. However, I think a simpler approach—that also requires fewer tokens cluttering up the configuration—is to do one manual push to gh-pages, after which the simpler workflow will work as expected. My suggestion is, before pushing to the master branch on GitHub, push your local copy to the remote gh-pages branch, as shown below.

git push origin master:gh-pages

That's enough for GitHub to recognize that you will be deploying to that branch. Follow that up with a push to the remote master branch as usual, and you should be all set.

Wednesday, June 27, 2018

Noise comments in Android Studio with Flutter

Flutter sounds neat, doesn't it? I decided to take a look yesterday, and it distracted me quite a bit from my course revision work. With my course revision and reflective blog post done, I decided to get into the Flutter codelab and see what I could do.

Step one is basically to copy and paste this code into Android Studio:
OK, copy, paste, and I get this abomination:
What the...?!

Those end-of-line comments are clearly not in the code. I do not want them. They are concretely, undeniably, irrevocably bad.

My first thought was, "I need to get Android Studio not to add those comments when I paste in code." I tried Googling for variations on this, but to no avail. Maybe I was calling it the wrong thing? I searched the preferences in Android Studio but couldn't find a good match either. Not wanting to waste more time, I decided I'd just go in and manually eliminate them for now.

I cannot.

They are not actually content in the editor! Those characters are not in the file. The view of the file is showing me data that do not exist. OK, stop and think about that, think about that for a long time if you have to. If you added this feature to Android Studio in particular, think about this for a long, long, long time. This is the worst idea.

But it's not the first time I've seen it. A few versions ago, IntelliJ IDEA added a "feature" in which it would show formal parameter names at the site of the actual parameter. It looks like this:

The text "bar:" is what they call a "parameter hint." It is not selectable or erasable. It's not really there in the data—once again, the view of the file is showing something that's not in the file. When I first saw this, I was aghast, and I could feel it in my bones that bad things were coming. I make all my students turn this abomination of a feature off.

Now, we see that my intuition was right. It's getting worse. Now, the view of my Dart source code file isn't just showing data that's not there: it's rendering that data as if it were. It's not using a different font or size, as in the parameter hints.

The good news is that this helped me consider where I might turn this off, and sharing this knowledge is the primary reason for this post. If you understand a modicum of what it means to write clean and maintainable code, then go to the File Menu, open the Settings dialog, and go to the Appearance section (with the Flutter Plug-in enabled, of course).


There it is. Untick that box and accept the changes, and your source code editor will be purified.

You're welcome.