A Weird Imagination

Devlog: Schedule Grid Editor

The problem#

Schedule grids (example) are a way of displaying a collection of events, some of which occur at overlapping times. They are often used to show what is happening at a conference or other busy event with multiple things going on simultaneously. They are tables where the y-axis is time and the x-axis is sometimes arbitrary or sometimes some concept of location (e.g., which room the event occurs in). Events are rectangles spanning their start through end times and usually covering a single column. This is a fairly standard display format for a calendar application showing a single day's events.

I had a friend who needed schedule grids for their job as a teacher in a (somewhat unusual) classroom that had a lot of small group activities, keeping track of where every student and staff person was supposed to be at all times. They were creating the grids using Google Sheets and spending a lot of time on the layout manually rearranging the columns and manually creating copies of the information to display both a summary and separate schedules for each person.

One complication was that due to privacy laws around information about students, I didn't want any of that data to be touching my server, both because I shouldn't have that data and I don't want to be responsible for promising the school district my server won't get hacked.

The solution#

Schedule Grid Editor (source) is a browser-based tool for creating printable schedule grids. While hosted on my server, the tool works just as well entirely offline. It saves the data in local JSON files (or locally in the browser using OPFS with support for import/export of JSON files).

It maintains a weekly schedule, where each event may be recur on one or more days of the week. Each event has some subset of the students and staff assigned to it, and the logic checks that no one is expected to be at two events simultaneously and that every student is assigned to some event at all times. For each day of the week, it generates a schedule with as few columns as possible showing all of the events. Additionally, for each day of the week, for each person, it generates a single column schedule showing just their events.

Read more…

Devlog: Resistance: Avalon web app

The problem#

I had been playing a lot of the social-deduction game The Resistance (and the version with more roles The Resistance: Avalon) and running into the problem that many players had trouble remembering exactly what had happened in previous rounds. Between the fact that there can be several votes throughout a game of the The Resistance and the game can sometimes take up to an hour, it can be hard to remember how the votes went a couple rounds back, especially for teams that were rejected.

The solution#

I created a web app implementation of The Resistance: Avalon (source) using Django.

Read more…

Devlog: Sprit Island helper

The problem#

The board game Spirit Island has all of players playing more or less simultaneously, especially when acting on different parts of the map, but requires some bookkeeping to be kept among all of the players. For a normal game of at most four players, this isn't difficult, but the game has rules to allow combining multiple copies to a huge game. My friend group planned a 12-player game and we wanted to figure out how to best keep the game organized. (Unfortunately, we planned this game for April 2020 and it did not happen for obvious reasons.)

The solution#

I developed a web app called fear-tracker (source), so called because the main shared information to keep track of is how much fear each player generates in order to keep track of the correct total generated by all players. It supports entering the fear generated per-spirit and the data is synchronized among any number of devices, so there does not have to be an exact correspondence between players and client devices. It also keeps track of what the current phase is as all of the players have to agree on some synchronization points for when new information is revealed by drawing cards.

The details#

Read more…

React to reacting to Discord join message

Posted in

The problem#

I run a Discord community for a local hobby-focused friends group. Since it's theoretically people I know in person, I don't want it to be an open invite; I want confirmation that everyone that joins has a real in-person connection to the group. I handle by gating access to most of the channels behind a role that I grant to new users once I've identified them. As the group has grown, I am often not directly connected to everyone joining, so I wanted an easy way to grant trusted users the ability to do the same. The straightforward way is to just use Discord permissions to let other people assign roles, but I wanted something smoother and easier for non-technical users.

The solution#

YAGPDB ("Yet Another General Purpose Discord Bot") is an extremely flexible and configurable Discord bot that can be set up to do pretty much anything. For this purpose, we're deep in its customization features which include a programming language for running custom scripts in response to various triggers. Since people in the community tend to 👋 react to the join message Discord generates when someone they know joins, I wanted to specifically make that the signal that a user is a real person. Here's a YAGPDB custom command that will do so when set up to be triggered by adding a reaction and configured with the desired allowed roles and filling in the configurable values at the top of the script:

{{/*
    Grant role to user when trusted user
    reacts to their join message.
    By Daniel Perelman <https://github.com/dperelman/>

    Loosely based on
    <https://yagpdb-cc.github.io/utilities/reaction-logs>
    by Satty9361 <https://github.com/Satty9361>
*/}}

{{/* Configurable values */}}
{{$logging_channel_id := }}
{{$roleName := }}
{{$adminUserId := }}
{{/* End of configurable values */}}

{{/* Actual CODE */}}
{{ if (and
        (eq .ReactionMessage.Type 7)
        (eq .Reaction.Emoji.APIName "👋")) }}
    {{$newUser := .ReactionMessage.Author}}
    {{$role := getRole $roleName}}
    {{$adminUserLink := (print "[" (userArg $adminUserId) "]"
        "(<https://discord.com/users/" $adminUserId ">)")}}
    {{$userLink := (print "[" .User "]"
        "(<https://discord.com/users/" .User.ID ">)")}}
    {{$newUserLink := (print "[" $newUser "]"
        "(<https://discord.com/users/" $newUser.ID ">)")}}
    {{ if targetHasRoleID $newUser.ID $role.ID }}
        {{/* sendMessage $logging_channel_id (print
            "DEBUG: " $userLink " would have granted `"
            $role.Name "` to " $newUserLink
            " but they already have that role.") */}}
    {{ else }}
        {{ giveRoleID $newUser.ID $role.ID }}
        {{ sendMessage $logging_channel_id (print
            $userLink " granted `" $role.Name "` to
            " $newUserLink " by reacting :wave: to their "
            "join message.") }}
        {{ sendDM (print
            "You have granted " $newUserLink " the role `"
            $role.Name "` by :wave: reacting to their "
            "join message. If this was in error, "
            "please contact " $adminUserLink ".") }}
    {{ end }}
{{ end }}

The details#

Read more…

Extracting Tametsi puzzles

The problem#

Tametsi (available on Steam1) is a great logic puzzle game that is a collection of Minesweeper puzzles that can be solved without guessing. The game consists of 100 puzzles plus 60 "bonus" puzzles. The bonus puzzles are in the game directory in an XML-based format that another player has documented well enough that they have even created some puzzles of their own and a viewer for those files. But the base 100 puzzles are nowhere to be found in the puzzle directory, and I had ideas for doing something with them.

The solution#

Given the file DumpPuzzles.java:

public class DumpPuzzles {
    public static void main(String[] args) {
        // Loading puzzles sets the graph on
        //  Game's MouseHandler, so it has to exist.
        game.Game.mh = new io.MouseHandler(null);

        for (int i = 1; i <= 111; i++) {
            puzzle.PuzzleOut.writePuzzle(
                new puzzle.Puzzle(i),
                String.format("puzzle_%03d.puz", i));
        }
    }
}

put it in the same directory as tametsi.exe2 and run

$ javac -classpath tametsi.exe DumpPuzzles.java 
$ java -classpath tametsi.exe:. DumpPuzzles

Then the puzzles/ directory will be full of files named puzzle_001.puz, etc.

The details#

Read more…

Devlog: Supply Challenge Plus (2 of 2): implementation

Posted in

The problem#

Last week, I talked about the modifications I and others wanted to make to the Supply Challenge scenario that comes with Factorio. This week I'll talk about actually making those modifications.

The solution#

If you just want to play the modified version, you can install my mod Supply Challenge Plus (source code on GitHub). The Mod Portal page describes what changes it makes and has screenshots of the UI with various choices for the settings to customize it.

The details#

Read more…

Devlog: Supply Challenge Plus (1 of 2): requirements gathering

The problem#

Factorio comes with a scenario called Supply Challenge which is a shorter, more directed experience than the standard "Freeplay" game mode. It replaces the pressure from enemies attacking your base with a series of timed requests where you have to provided a pre-defined set of items within a time limit with a new request every several minutes. This both can be good for new players to have guidance on what they should be working on next and for experienced players as getting everything done within the time limit can be, as the name suggests, a challenge. As those are two somewhat opposing goals, I wanted to add settings to make it better for both use cases.

But first was the question that precedes many coding projects: has someone already done this?1 And the related question: has anyone suggested doing it and what features did they find important that might be worth considering in the design?

Why?#

For a personal project that possibly no one else is going to use, it's not immediately obvious why I care what features other people might want. But there's a few reasons such a search can be valuable in addition to the obvious that other people might use what I create. First, finding other users wanting the same features I want is validation that those features are good ideas. Other people may have thought of features that I hadn't thought to implement but actually want. And even for features that I am not interested in implementing at the moment, keeping them in mind may affect the design.

Initial ideas#

Read more…

Metatables for Factorio reflection mod

The problem#

Using my Factorio reflection library discussed previously involves interacting with Lua values that are a combination of the actual value and some metadata, so you have to know about those values to use them. Worse, the interactions I defined are quite verbose. The main thing you're like to want to do on a value is lookup a property on it. Normally in Lua that looks like

table[key]

but if instead of table you have a wrapped value from the reflection library, you would look up key on it with

ReflectionLibraryMod.typed_object_lookup_property(
    wrapped, key).value

If you want to do multiple levels of property lookups, then this quickly gets quite unwieldy.

The solution#

Lua supports operator overloading through a mechanism it calls metatables (some additional examples).

Using that mechanism, the library defines a value ReflectionLibraryMod.typed_data_raw that can be indexed as wrapped[key] and assigned to like wrapped[key] = newValue.

The basic setup looks like

local prototype = {} -- table for methods
local mt = {}
mt.__index = function (table, key)
  local res = prototype[key]
    or ReflectionLibraryMod.wrap_typed_object(
      ReflectionLibraryMod.typed_object_lookup_property(
        table._private, key))
  if res == nil then
    if key == "_value" then
      res = table._private.value
    end
  end
  return res
end

mt.__newindex = function (table, key, newValue)
  -- If newValue is a wrapped typed value, then unwrap it.
  if getmetatable(newValue) == mt then
    newValue = newValue._private.value
  end
  table._private.value[key] = newValue
end

function ReflectionLibraryMod.wrap_typed_object(typedValue)
  if typedValue == nil then
    return nil
  end

  local res = {_private = typedValue}
  setmetatable(res, mt)

  return res
end

Any additional properties would be defined next to the definition of _value. And any methods would be defined on prototype.

The details#

Read more…

Devlog: Factorio reflection mod

The problem#

When developing the "garbage collector" for the Pacifist mod, I noted that I couldn't actually know which strings should be treated as references. As a workaround, I just assumed all strings were references, which worked well enough, but I wondered if there was a way to get more precise type information. Additionally, when I was trying to figure this out, the developer of exfret's randomizer expressed interest in getting access to such information for that mod.

The solution#

The Factorio documentation includes a machine-readable version of the prototype API documentation. My ReflectionLibrary mod provides access to that information from within a Factorio mod, effectively faking a reflection API for type information on data.raw during the Prototype Stage:

local data_raw = ReflectionLibraryMod.typed_data_raw

local bb = data_raw['blueprint-book']['blueprint-book']
log(bb.inventory_size._type.typeKind)  # prints "literal"
bb.inventory_size = 42
log(bb.inventory_size._type.typeKind)  # prints "alias"
log(bb.inventory_size._type.name)   # prints "ItemStackIndex"

The details#

Read more…

JSON to Lua

The problem#

The Factorio documentation includes a machine-readable version of the prototype API documentation. I wanted to be able to access that information from within a mod, which required somehow giving Lua access to a JSON object.

The solution#

Install my branch of json2lua and run it on your JSON file:

$ npm install git+https://github.com/dperelman/json2lua.git\#feature/string-escaping
$ npx json2lua "FILE.json" "FILE.lua"

Note the output is a Lua table literal, so you'll have to add code to actually assign it to a variable to be able to use it.

The details#

Read more…