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.

Tuesday, July 21, 2020

Using emacs as a commit editor from GitBash

For some reason, perhaps related to tinkering with Flutter, my commit editor settings went wonky on Windows. Trying to rebase from Git Bash was connecting to Visual Studio Code rather than Emacs. Using Emacs is an important part of my workflow because of the ease with which rectangles of text can be replaced, such as when choosing to squash sequences of commits. (That's M-x replace-rectangle for the curious.)
It took much too long for me to get the proper setting back in place to run emacs as my commit editor, so I'm tracking it here, in case I need it later. Here's the command:

git config --global core.editor "c:/program\ files/emacs/bin/emacsclient.exe -c -a ''"

Yes, all that punctuation at the end is significant.

UPDATE: It turns out this isn't quite right. It works once, but then on subsequent rebases, emacs opens with an empty buffer and a message that the file on disk has changed. I didn't have to do this before, so whatever configuration I had before that worked, this wasn't it.

Monday, July 13, 2020

Answering questions about effective online learning

I was asked, through the grapevine, to provide some insights into online teaching and learning. The answers are being compiled into an article directed at parents of first-time college students. In the spirit of no wasted writing, I decided to write up my answers here.

Question 1: From your experience during the Spring semester, what approaches to online learning taken by students seemed to be most successful for them?

I want to preface my answer my saying that both my Spring courses were team-oriented, project-intensive courses. We had a great advantage in that the teams were formed before the pandemic forced campus to close. I am planning to do less team work than I normally would since it is so hard to mentor distributed teams as they learn fundamental technical and social processes of collaboration.

A common feature of my most successful student teams was that they maintained a regular schedule. They all had this before the pandemic, and many of them carried that schedule forward as classes moved to purely online. They established regular meeting times, generally between three and six hours, during which times all team members were expected to be online and working on the project. Some used Zoom audio or video meetings during the whole time, while others used Discord or Slack, switching to Zoom audio or video calling when text was inadequate. Because the whole team was online at the same time, teams were able to clear up communication quickly, before issues festered and became social and technical problems.

The most successful teams combined synchronous distributed meetings with asynchronous, persistent chatrooms—either Slack or Discord. This gave a place where they could pose questions, provide links, encourage each other, and otherwise socialize outside of their scheduled meeting times. In the cases where I was invited to join and participate in these discussions, I definitely saw the best students doing all four of those things. The impact of peer-encouragement on these forums is not to be underestimated. When students took the time to give each other a little virtual pat on the back, it clearly and directly contributed to a higher morale, better learning, and increased productivity.

Students and teams who reached out to me for help had much better results than those who didn't. This should come as no surprise, as it's the same for in-person classes, but it also should not be discounted. I could answer easy questions over email, and anything more involved could be done through a video chat—ideally with the whole team. In a normal semester, I would set up face-to-face meetings for this purpose, but video chat actually proved much easier, and I plan to keep this tool on my toolbelt moving forward, regardless of instructional mode.

Question 2: Conversely, were there certain approaches to online learning on the part of  students that caused them difficulty or led to unsuccessful outcomes?

After campus shut down, a few of my student teams decided to take this as an opportunity to relax their schedules. Rather than meet together to work on the project at specific times, they divvied out responsibilities and asked people to work whenever they felt like it. This led to the unfortunate but seemingly-inevitable consequences of miscommunication and confusion, which then resulted in mistrust and poor performance. Briefly, every team that relaxed their schedule of meetings regretted it.

Incidentally, if you ask any undergraduate what the worst part of college is, the answer is "group projects." There are many reasons for this, and they include that we don't do well, institutionally, at teaching students how to work in groups. I had an upper-division class in Fall 2019 in which we had a powerful heart-to-heart conversation in which the room of students admitted they had never been on anything than a dysfunctional team in any school experience. That is, working on a team is not just intrinsically hard: the students also have no tacit or mental model for what positive collaboration looks like.

Coming back to the question, another problem I saw was students' using familiar tools rather than effective but novel tools. Discord vs. Zoom is a great example. Many students use Discord for online gaming, and since they are familiar with the tool, it becomes a natural choice for them when deciding on how to work together as a team. Discord even recently added screen-sharing, so that you can see what a teammate is working on. However, this is like streaming on Twitch: it's a one-way stream, in which one person is working and others are watching. Contrast this against Zoom's Screen Sharing feature, in which anyone on the team can actually drive the host's computer. This leads to real collaboration rather than simply passive or voyeuristic participation. In almost every online team meeting in the Spring, a case would come up where someone in the conversation (often me!) would take control of the screen and type in a few characters to explain a concept, or steer the mouse to demonstrate an interaction. This gives a similar affordance to sitting together in the lab or in office hours, temporarily "taking the wheel" to demonstrate something, as compared to the passive-consumptive stream approach, which as no affordance for collaborative work.

An important point about this is that some of my teams continued to use Discord even after I pointed out the problem or, in one case, mandated the use of something else. The team's inertia and pride kept them from learning about a better solution to their problem. I suppose what we see here is that the root cause is not the tool but the pride, but that seems to be always how it is.

Question 3: What are some tips you can offer students about how they can make the most of online learning and have the most successful outcomes?

Students are in many ways at the mercy of their instructors. The frustrating truth is that it can be hard to get faculty to make the most of learning toward the goal of successful outcomes. Because of this, students need to be careful consumers. To the extent possible, see what you can find out about the courses you want to take. See if you can infer from the designs that this is going to be good learning experience, based on the objectives of the course and the interactions designed for them. Personally, I keep all my course plans public and online, but I suspect I'm in the small minority there.

Advice that is easy to give and hard to swallow is be conscientious. Conscientiousness is highly correlated with academic success, and no matter how conscientious you are, you can always take action to improve. I think the most accessible way to do this is to set up a schedule for a week, see how well you can follow it, and then actively reflect on the differences between what you had hoped to do and what you did do. Nobody is going to make you do this reflection, but that is where the real learning happens.

It's also important to keep in mind a few core principles of effective learning, such as I wrote about in my notes about the excellent Make It Stick. These principles are not about success in college so much as how to become an effective lifetime learner. Ideally, these things would align, but in practice, they don't always do so. I'll repeat my quotation here from Chapter 8 of that book, which provides a great overview of the mindset of effective learning.

  • Some kinds of difficulties during learning help to make the learning stronger and better remembered.
  • When learning is easy, it is often superficial and soon forgotten.
  • Note all of our intellectual abilities are hardwired. In fact, when learning is effortful, it changes the brain, making new connections and increasing intellectual ability.
  • You learn better when you wrestle with new problems before being shown the solution, rather than the other way around.
  • To achieve excellence in any sphere, you must strive to surpass your current level of ability.
  • Striving, by its nature, often results in setbacks, and setbacks are often what provide essential information needed to adjust strategies to achieve mastery.
Finally, my advice to all learners is practice intellectual humility. Listen to other people as if they know something you don't know, because they do. 

Sunday, July 12, 2020

An afternoon with Fate

The Fate RPG caught my attention at the start of the pandemic when the publisher, Evil Hat, offered some sourcebooks for free. I can't even remember how I heard about that, to be honest, but it piqued my interest and I started reading some more about it. The different versions of the RPG confused me for a bit. I started by reading some Fate Core, then I found Fate Accelerated (FAE), which is a briefer version that, it turns out, is still in keeping with Core. I read through some of FAE, then learned about Fate Condensed, and this one seemed like it might be the right size for me to try out. However, even when reading Condensed, I found that I had to go back to Core to try to wrap my head around some essential ideas such as invoking aspects.

I gave my family a heads-up during an evening walk that I wanted to try running a one-shot of a fiction-first RPG with whomever was interested. I explained that it could be any setting at all, and I encouraged them to think of one that might be interesting. This afternoon, I invited any who were interested to actually sit down, make a world, create some characters, and try a session. My 13-year-old, 10-year-old, and 5-year-old all expressed interest, so we set down to it.

We batted around a few ideas for the world. The first suggestion was a sword-and-sorcery world, but I said I'd rather not use that since it's too familiar. (Also, I have secret plans to try Unlimited Dungeon with them.) Other ideas that were mentioned included steampunk, Star Wars, and age of exploration. I suggested we could combine the steampunk and exploration idea and make a world of floating cities and airships. We were all pretty excited about this, so we set into making characters. The eldest wanted to make a tinkerer, and he got started making Fredrick Sparksmith the Expert Mechanic without trouble. The second son had some trouble narrowing down what he wanted: some kind of fighter, thief, sneaky character. After some back and forth, he settled on the concept of "sneaky deserter." To establish his premise, we decided that the floating cities were all independent, and there was an ongoing war between the Kingdom of Terrinoth (the city's name taken from Runebound's world, of course) and the Republic of Loramor. His character, Danny Lorens, had been a soldier for Terrinoth who deserted after his team were nearly all wiped out. The team lacked a captain, so I asked the youngest if he would want to play the captain of an airship. We worked up Apple Cider (which was also the name of his Exspelled character), the Adventurous Captain. 

During this discussion, we returned to the world. I remembered having read in Fate Core that it's good to start with two setting aspects, potentially one current issue and one impending issue. The war between Terrinoth and Loramor was an obvious current issue, so I suggested we try to come up with an impending issue. At first, the boys focused on side-effects of the war, such as resource shortages and rebellions, but I suggested that these were sort of implied by the war and maybe we should try something separate. I was thinking a bit about Dungeon World fronts here. (I wrote about my first game of Dungeon World a few weeks ago.) I suggested something like a looming disaster, like an oncoming storm. The conversation turned to whether or not there really was a ground beneath the floating cities or not, and we decided there wasn't. One of the boys then suggested that there could be some kind of evil coming from the depths themselves, and we came up with the idea of the Shadow Creatures: beings that were increasingly coming up from the depths and attacking airships, and now everyone was afraid to fly or even go out at night.

After a brief recess, we got back together. I asked Apple Cider what kind of treasure he might be seeking, to which the answer was donuts. On one hand, that's a good answer, but on the other hand, I think we can see that there's a disconnect between the narrative desires of 5-year-olds and 10–13-year-olds. The little guy was awfully bored during character creation and actually had asked to leave. I worked out his skills for him and then invited him back to the table for the storytelling part. Even here, though, he did not last long. He changed his mind from donut to trophy, and so I set the initial scene as the group flying to a "scatter" of islands (our word for the equivalent of an airborne archipelago) that were currently being fought over by Terrinoth and Loramor. One of the islands held the treasure, or at least a key to it. However, the ship was stopped by a scouting party of Loramor soldiers, and were asked to identify themselves.

We had some fun with this encounter, and I'm of two minds about it now. My intention was to use it to explain the rules of play, since we really hadn't talked about them yet. I had explained only that aspects and skills were useful, and we had just skipped entirely over Stunts, figuring I could help guide them in those definitions as we played. I think the scenario was useful here, but it was also hardly in media res. I wonder if I should have done something more adventurous. 

They got to the floating island in the scatter and found an ancient vault when the scouts they saw previously got into a firefight with some from Terrinoth. As the heroes opened the vault, the battle escalated, and this put them into a spot: should they try to sail away and risk being seen as enemy combatants, or should they stay and risk being on the island at night? They chose the latter. 

Naturally, this is where I had some shadow creatures prowl around the ship. By this point, the five-year-old really had had enough and left, but I had invited by 7-year-old to join us and play Apple Cider. He was happy to do so, and he loved the game. I knew he would, given that he loves storytelling and he reads effortlessly. I'm sure all the text on the character sheets and index cards was frustrating at best for the littlest guy. In any case, Apple Cider had the aspect, "Takes on more than he can handle," so I compelled him to want to take a shot and see if he could capture one of these mysterious creatures. He loved that, and we started our only Conflict of the game. It was highly cinematic, and the boys loved being able to describe their actions, even if they found it challenging to improvise a compelling narrative. Highlights include: a shadow creature leaping onto a lamp and turning it from a "light" (that spreads light) into a "dark" (that spreads dark); Fredrick running in to turn off the "dark", but accepting a compel for his Scatterbrained aspect that made him forget the switches and just wildly turn things on and off; Danny getting a Success with Style to throw one of his daggers through a shadow creature and right into the "dark", shattering it.

This is where we wrapped it up, not exactly a cliffhanger but with more questions than answers. Thinking back, I clearly had "campaign" mindset rather than "one-shot" mindset. All the boys said they would happily play again, the older two especially feeling like we had made a really exciting and interesting world. 

Even with my one-page cheatsheet from the Condensed rules, I still found it hard to think about the conflict resolution rules. In particular, it wasn't always clear to me when I should simply set a difficulty for an Overcome vs. create a Challenge, Contest, or Conflict. I intend to re-read the corresponding portions of the rulebooks. After having taken a quick look at the FAE character sheets, I'm wondering if the reduced number of skills would actually make that a better fit for the table. Balsera-Style initiative, which I think everyone at the table enjoyed, could easily be imported from Condensed to FAE

Another problem I ran into is who makes choices in the face of failure. The one-page sheet leaves it ambiguous who makes the decision between Failure or Success at a Major Cost; the full rules explanation, however, makes it clear that it's a discussion with the GM and, potentially, the defending player. I realize too in cursory re-reading of a few pages of the rulebook that our conflict looked maybe too much like D&D combat rather than fiction-first. I need to remember to turn the narration over to the player rather than feeling like I have to take that all myself. It's not that I didn't do this right at all, but it's a matter of degrees, and the fiction-first approach is the direction I want to lean toward.

One area I can definitely improve is giving sensory descriptions of the scene and using these to define aspects. I really did not do this enough, focusing more on ensuring I had the rules right in my head than providing a compelling scenario description. 

Another problem I should have seen coming is that I could not read the other players' character sheets during the game. I really need to have my own copy of their aspects so that I can more readily scan them for opportunities to compel.

In summary, we had a great time, and we all enjoyed the world that we created. Part of me would like to go back there, but part of me would also like to try something completely different, maybe with FAE, to see what the boys think. The two younger ones definitely were excited by the combat, but it was really fun to see how the older one recognized he didn't have to stand and shoot: he could instead modify the environment.

Thursday, July 9, 2020

Deploying Godot Engine Web Games to GitHub Pages with Continuous Integration

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

After figuring out how to use continuous integration with Flutter Web apps on GitHub Pages, I turned my attention to Godot Engine. Through my FamJam projects, I have developed a pair of scripts (build.sh and deploy.sh
) for building and deploying games to GitHub Pages. It seemed like a natural extension to see if I could do this automatically. This could be useful both for my Fam Jams and, of course, for my revised game programming course.

In addition to GitHub actions, the other two critical pieces to getting this to work are aBARICHELLO's Godot CI Docker image and, as used in my Flutter approach, peaceiris' GitHub Pages Action. Here is the simple version:

My custom build script usse NatrimCZ's approach of reducing project download size by zipping the project files and extracting them client-side, and it was surprisingly straightforward to incorporate that into an alternative GitHub workflow:

Either approach, just like with Flutter, requires you to have an authoritative push to gh-pages before using GITHUB_TOKEN for authentication will work; once again, you can do something like this to get that done:

git push origin master:gh-pages
Also, as mentioned in the comments, the script assumes that you have already configured the project with HTML5 export and that you have set the export location to build/web.

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.

Tuesday, July 7, 2020

Summer Course Revisions 2020: CS445 Human-Computer Interaction

After wrapping up my preliminary planning for my Game Programming course before the holiday, I turned my attention to the necessary revisions for my Human-Computer Interaction course. I had a few ideas swimming around my mind, but I decided it would be best to go back to my notes from last time I taught the course, which was Fall 2019. I re-read my end-of-semester blog post, and this reminded me of how terrible that semester was. I had sort of forgotten how painful that course was, but it was good to be reminded.

I decided not to include a community partner for the Fall. I have been disappointed with what my students have been able to provide in this course. While the pandemic makes it easier to work with partners who are distant, it also removes the crucial ability to sit in a circle and have an honest chat. The risks are just too high right now to ask an external partner to donate their time to the students. Of course, if I get good learning outcomes from the students, I would love to have them engage with a community partner in subsequent experiences (such as the game studio course). The topics are just too hard for the students to grasp to ask them to be able to learn them and apply them to a partner's benefit in one semester, given all the other constraints.

The course plan that I just put online lays out the first several weeks of the course in good detail. During these weeks, students will be reading Don Norman's The Design of Everyday Things and completing weekly exercises to help them build their understanding. I have used this book in the past several semesters, and I like that the students can both learn from it and practice learning from expert writings (rather than conventional “textbooks”). The weekly exercises are all in a format that involves individual reading and work from Thursday to Tuesday followed by a period of online peer review from Tuesday to Thursday. In this way, I hope to simulate some of the discussions and ad hoc peer reviews that I would normally do face-to-face in the classroom.

I have been disappointed in the past with students' ability to bring knowledge from DOET into their future work in the semester, and so I added a new culminating assignment that asks them to apply the book's principles to their CS222 projects. In part, this copies the inspiration I use in CS222 when I have them look at their CS121 projects: having them look back on their work, after they have had a chance to step away from it, should give them more objectivity while also reminding them that, despite imperfections, that work was important to their journey. This report is currently designed as a two-week writing assignment with an explicit, graded check-in point halfway through. This will give me and the students' peers an opportunity to give them feedback on which parts of the report are working and which are unclear before the students complete their final submissions.

The course plan as of this writing only goes through the DOET readings and analyses, but my planning notes go several weeks farther. My plan is to get them into Flutter, specifically the Web support that is currently in the beta channel. The thing I like about Flutter is the “code as design” approach, in which the code clearly reflects the application structure. I have seen many students in this course and CS222 get hooked on a drag-and-drop UI builder such as scene builder, leading to two dangerous outcomes: they lose the insights and power about software structures that are gained through programming, and they still design bad-looking interfaces. I'm hopeful that Flutter will give us the right level of abstraction to avoid these two pitfalls.

What I would like to do is have them learn Flutter by creating a simple project, which at this point will likely be a grade calculator. It's simple and familiar, but there is still plenty of room for students to go in different design directions. Then, I would like to use this simple example to introduce readings and techniques of usability evaluation, Gestalt vision, and accessibility. That last point is a new one for the course despite its importance: it has frequently been one of the topics I have cut when doing community-engaged projects because, inevitably with community projects, there are problems that the teams need to put their heads down on and fix.

I would like to wrap up the semester by having them make a larger project that they can use to bring together all the analytical tools and techniques that they have learned. I am still a little uncertain about the context for this project. I may just open it up to them, as I do in CS222, but this runs the risk of students falling into a quagmire of code and not appropriately deploying the other cognitive tools we are learning. I may just give them a canned project for that reason. Two that have come up recently in my own practice are a tool to judge student showcase entries, as in the CCSC Midwest Conference, or a tool to help students manage checklist-based specifications grading, as in my Game Programming course plan.

I think I will return to the CS445 course plan in the next few days and add details for those other assignments. Looking at it now, I realize that I forgot to add the due dates to the Web site; since I cannot yet even access the Canvas page for the course, I may as well trick out my lit-html templates to include the deadlines online. In any case, I had a productive morning of planning, and it seemed like a good time to write this blog post in the gap before lunch.

As always, thanks for reading, and feel free to share any feedback or questions you have about the plans.

Thursday, July 2, 2020

Family Painting: Arcadia Quest - Beyond the Grave

Remember how much fun the G-Force had painting Arcadia Quest and then the Riders expansion? Well, while we were playing through Riders, I saw that Beyond the Grave went on a great sale online, so I jumped on it. I had not picked up my brushes for a bit, but once I primed the Beyond the Grave minis and showed them to the kids, they were very excited to get going. I think we painted this set in just over a week. Hurrah for summer schedule!

We started with the Skelebones, which come in two sculpts.

Skelebones by me, my wife, and #3 Son
Skelebones by sons #1, #2, and #4


I think we all expected these to be standard skeleton minis but they're really not: they have arms and legs rather than just bones. This is indeed some dark magic. I thought they were pretty fun to paint and that everyone did a nice job. Notice the nice glowing eyes on #1 Son's model. I should also mention that I left #4 son (who is 5) to his own devices most of the time, leaving him to mix and thin his paints. Sometimes I would notice that he had not thinned nor thought to mix them and give him a hand, but this is all pretty much his own work. Not too bad for a little guy!

Slasher Zombies by me and my wife

The Skelebones were the only minion with six copies, so in the next painting session, my wife and I completed the Slasher Zombies shown above. Again, I had a good time with this one. For the armor, I painted it all in purple then undercoated the trim in white before painting it in red. I believe my wife tried to leave the primer showing for the red on hers and had more frustration. I like how the face turned out on mine, and I think my wife did a great job on the facial highlights on hers, too. I think she wanted the brains on hers to be more pink but had a hard time getting a good tone. For mine, I had a pretty good plan going into it, starting with a mid pink, darkening the recesses, and highlighting to get a wet look.

Axe Flinger Zombies by the Boys (in age order)

While Mom and Dad worked on the Slasher Zombies, the boys worked on these much simpler Axe Flinger zombies. #3 Son interpreted the skin color to be much brighter than his brothers did, but that's fine. Purple skin and white hair is taken from the card art, but it is kind of a strange combination. #1 Son is doing a great job with his highlights and shadows these days, and I think we can see that #2 Son is on the cusp of this as well. If he took a bit more time to drop a little Nuln Oil around those eyes, for example, he would get a lot of bang for his buck. He's ten years old and doesn't like to sit and focus on a mini as long as I do, but I think as he develops patience and if he has desire, he could reach the next level. Also, I would be remiss not to say those are darned good pupils by the youngest boy.

Necromancers by #1 and #4 Sons

The next painting session was also a 4-2 split on models, and I let them pick their preferences in ascending age order. Sons #1 and #4 both picked Necromancers. Once again, #1 son really nailed it, and I think the nice paint job is accentuated by a nicely balanced color scheme. On the right is a really great job for a five year old, on a figure that I can't help but call "The Necromancer with the Derpy Eyes." He may show up in a campaign someday.

Ghosts by me, my wife, #2 Son, and #3 Son

I knew when I first opened up the game that the ghosts would be an interesting painting challenge, given that they are practically monochromatic. Four of us painted them while the other two worked on the Necromancers. The shade color on mine is arguably too dark for a ghost; it's certainly a bit darker than I intended, which I didn't realize until my wife asked about it and we held our side by side. It makes my ghost look a bit more manic, but that's not necessarily bad. #3 Son saw it all as basically one tone, but #2 Son did a nice job getting variation in his. I don't know if he missed the red/magenta spots or purposefully left them blue.

You cannot tell so well from the picture, but my wife saw a Bride of Frankenstein streak in the ghost's hair that I didn't notice. It's a streak of dark blue on the left side of the head. In the card art, I only read it as a shadow, but when I looked at her paint job, I think she captured what the artist intended. 

At this point, we were out of minions, and the rest of the painting was done in the evenings by my wife, my two older boys, and me. Here they are in no particular order.

The Black Brothers by #1 Son

My eldest son wanted to paint the figure on the right, and he basically finished it one night, so we also gave him the one on the left to paint. A positive result of this is that they really look like they go together. For the black armor, he went for a non-metallic metals (NMM) approach, which I guess I didn't really understand when he asked what I thought about NMM. Personally, I avoid it because it's so hard to get right. I didn't realize it was what he was going for here; I thought it was more of a painted or patina armor. In any case, I think for tabletop quality it's great: the bright white highlights sell the idea of glint. Looking closer, it doesn't have quite the dynamic range to sell it, but hey, who's looking closer? I think the paint job is helped by the fact that the armor is not really noteworthy, so it's easy to read as "black armor." The facial expressions of these two characters are the focal points, and he really nailed those parts. The swords, too, are much more interesting than I initially gave them credit for: he got a nice shine on them that does not draw attention but really sells the story.

Frank by my wife

My wife did a great job on Frank, although I think she doesn't give herself enough credit for it. The card art for Frank is, frankly, boring. Who puts a white shirt on a flesh golem? The sculpt is also rather featureless, just kind of bulbous. She struggled with white—who doesn't?—and was concerned that the result looks dirty. Personally, I think it's great for someone who only paints every few months at the kitchen table. 

The real seller here though is the bottom of the shoe. Get this: it is a perfectly flat sculpt. She painted in the texture that you can see on the picture. I had no idea until she pointed this out. Nice work, honey!

Dread King by #2 Son

The Dread King is the Big Bad from this expansion. The card art is compelling, and I think the sculpt, while maintaining the chibi style, is exciting. Before I had the chance to ask any of the boys if they had preferences, #2 Son asked if he could paint this one. I think he did a nice job on it, especially the flesh tones, where you can see his efforts at highlighting. He asked for advice halfway through, and I encouraged him to use the Nuln Oil and Agrax Earthshade (the only things we use besides the Vallejo Basic Colors set) to get more contrast around the teeth. Originally, they were hard to distinguish from the rest of the skill, but he got just the right amount of contrast to make them pop.

At this point, you may be wondering, "Paul, was there anything that made you buy this set besides the fun of family painting and of playing the game?" It turns out that there was. When I was considering whether or not to pull the trigger on this one, my brother informed me that there were sculpts based on characters from Young Frankenstein. Well, that's easy.
Dr. Spider and Ivan, painted by me

I painted Dr. Spider with the rest of the family as we worked on the figures shown above, but Ivan I actually painted at my regular painting station one evening while catching up on some podcasts. 

The first night working on Dr. Spider was spent entirely on his face, and I am really happy with how it turned out. The only real bummer here is that the card art features kinky Gene Wilder hair, but the sculpt has a completely different texture. Sure, it was probably easier to sculpt than curls, but it's not Gene Wilder hair. The serum he is holding was painted using techniques from this article, which I regularly reference for such things.

Ivan was pretty straightforward, once again the most time being spent on his face. It wasn't until I was painting the eyes that I discovered another very disappointing difference between the art and the sculpt. The card art clearly has Marty Feldman eyes, but the sculpt just his big eyes facing forward. It wasn't just a matter of painting the eyes, either, since the pupils are sculpted in. If I had noticed this ahead of time, I would have filled the pupils and redrilled them (or at least painted them) off in different directions. It's a lost opportunity on an otherwise fun model. The brains turned out fine, and I'm especially happy with the redness on the nose and cheeks.

In summary, then, a great time with the family, a fun time painting, but a minor disappointment that the figures don't look more like Gene Wilder and Marty Feldman. The older two boys are chomping at the bit to play, and so I imagine we'll get this to the table tonight. 

Thanks for reading!