Using AWS Amplify to Rapid Prototype a Real-Time Dungeons&Dragons Map

Using AWS Amplify to Rapid Prototype a Real-Time Dungeons&Dragons Map

Would you like to play a game?

ยท

19 min read

TL;DR -

Hey, I'm Matt. I'm an AWS Community Builder and a 24/7 geek. Over the last few years my friends and I have had a weekly virtual (pre-pandemic) game of Dungeons and Dragons where maps have always been a problem. Originally we tried Roll20 and other solutions, but had issues with all of them. At this point we're heavily invested in D&DBeyond and use them for character sheets, but they don't have good map support. Over the last few years we've been using Google Slides for maps ๐Ÿ™ˆ

Around once a year I decide to experiment with some way to improve our game play and this year it happened to coincide with an AWS Amplify + Hashnode Hackathon. I've tried making character sheets before and doing more of a full-game dice-roller replacement, but at this point D&DBeyond does pretty well with that. I'd even hacked together my own discord integration via a Google Chrome extension which works fairly well. There are better options for that now too.

There still isn't a map solution I'm happy with. As an engineer, I'm actually surprisingly familiar with maps. In my Navy career I did a bit with them as complicated as doing geodetic conversions for line of sight / over-the-horizon calculations for aircraft communicating with sonobuoys and for a year in grad school I was a TA for an Artificial Intelligence for Robotics course that had a lot to do with maps for A* among other things. Dungeons and Dragons operates on a grid-based system out of the box, so this ended up being a perfect storm โ›ˆ.

The hackathon announcement came as we were nearing the end of our current campaign and talking about what the next campaign would be. To me this was an opportunity ๐Ÿ’ก. We've always struggled with maps and lack of role play because things tended to be revealed to the entire party OR we saw the entire map ahead of time because we didn't have individual lighting / fog of war.

Over the last year my work has focused on DynamoDB + Serverless frameworks. I've barely touched react over the last year and and have zero experience with AWS Amplify. I know TailwindCSS has gotten popular and never got around to trying out GraphQL, so my goals on the tech-side of the hackathon were fairly simple:

  • โœ… Use AWS Amplify to rapid prototype a real-time map ๐Ÿ”ฅ
  • โœ… Try out GraphQL (conveniently part of Amplify / AppSync) ๐Ÿง
  • โœ… Try out TailwindCSS ๐Ÿ‘€
  • โœ… Dust off react ๐Ÿคทโ€โ™‚๏ธ

As far as the app goes, it needed to:

  • โœ… Support real-time, virtual play on a map
  • โœ… Allow players to have their own unique perspective on a map (unique player vision)
  • โœ… Allow the Dungeon Master control of everything (global DM vision)
  • โœ… Calculate move distances on a tile grid
  • โœ… Enforce player initiative

Things I learned or reinforced by working on this project:

  • ๐Ÿ˜ฑ Good reminder that I am not a designer
  • ๐Ÿš€ Amplify is amazing for rapid prototyping
  • ๐Ÿง I still have a lot to learn

Dungeons and What?

If you've never played Dungeons & Dragons before here's a quick primer so the rest of the post makes a little sense. It's predominantly about story telling... don't let the rules lawyers tell you it's about following the rules.

One person acts as the Dungeon Master (DM) and they control the narrative. Players act in game as Characters who have their own backstories, motivations etc. When a Character wants to do something, they narrate it and on occasion the Dungeon Master will have them roll a 20-sided die (a D20) to determine whether they were successful or not.

In practice this is all done out-of-character which (in my mind) takes the players out of the immersion. That's part of what I want to fix.

If you'd like to learn more about Dungeons and Dragons check out the rules introduction or listen to one of the many DnD podcasts such as Critical Role which is done by professional voice actors.

Amplify-DND

The app lives here. You're welcome to create an account and make a game (public or private) or mess around with the public game I've created. I can't guarantee the site will be up forever, but if you're interested hit me on Twitter @martzcodes.

To do anything on the site, you'll need to create an account. This is using the default Amplify components for Auth (which is why you oddly need to create an email-based username AND provide a username).

Create a game by going to "My Games", entering a name and clicking Create Game. You'll be redirected to the Dungeon Master (DM) page. Let's add two characters... "Tiddlywinks McGibbons and "M as in Mancy". Click the + in their rows to add them to the initiative list. I placed TM at 1,1 and Mancy in 1,2. Next we need to place them in a room.

Screen Shot 2021-02-27 at 11.50.05 PM.png

On the left, change Column One to "Rooms" and Column Two to Doors. I created a room at 7x7 room at 0,0 with a 3x3 LAVA field starting at 2,2. Then I created a room starting at 7,1 that's 5x5. It looks like this:

Screen Shot 2021-02-28 at 12.24.25 AM.png

But we need a door to go from one room to the other. To help let's click the "Show Points" checkbox and add the door to (6,3).

Screen Shot 2021-02-28 at 12.26.19 AM.png

On the character list you can click the link button to view the perspective of those players, and these are the links you'd share out to the players. Each player can see different things.

Screen Shot 2021-02-28 at 12.29.01 AM.png

Perfect! The green shaded areas are move distances (based on player speed) and they have their own perspectives. The AppSync side handles the real-time aspect and we have player initiative. Seeing parts of the map is great, but I also want the players to be able to describe different things. That's what areas are for. As a DM at certain intervals I'd update the player's perception score. Right now Mancy has a Perception of 15 and Tiddlywinks has a Perception of 10. MAYBE the door in the room is hidden (change that in the door settings). The room they're in is hot, they both know that. But maybe only Mancy sees the door. I hid the door, closed it and reset both of the characters vision (to reset the fog of war). Then I added two areas. One with a Perception of 0 that they can both see, and one with a Perception of 15 that only Many can see. Now this is the view:

Screen Shot 2021-02-28 at 12.38.11 AM.png

Tiddlywinks sees flavor text highlighted by their own color because they have enough perception to observe something. "It looks like there's a door on the East Wall". As DM I set this up ahead of time. I dont have to remember to private message a player or announce to the world that they see something. The person playing Tiddlywinks can read this automatically and say in game "Hey, I think there's a door over there". From there we can do the normal DND stuff... Roll investigation and the door is revealed and the game continues.

As DM I'd set up most of this ahead of time, but I also have the flexibility to add rooms / doors / hide and unhide things and add flavor-text areas on-demand, synced between different computers in real-time. ๐Ÿคฏ

How'd I make it?

I spent around 40 hours working on this project. I'd guess the breakdown was:

  • 65% - Tweaking CSS / Styles / UI / UX
  • 15% - Figuring out and Implementing GraphQL
  • 10% - Remembering A* and making the code more efficient
  • 5% - Writing the actual blog post
  • 5% - Using Amplify

Emphasis on that last part: Amplify was SUPER easy to use and there are a lot of concepts in it that I like as far as the "Developer Experience" goes. I don't agree with some of their design decisions, but it was easy and I'd argue for rapid prototyping that's far more important.

I started by using the Build a Full-Stack React Application tutorial which was 90% of what I needed on the Amplify side. The rest was tweaking React code, GraphQL schema and CSS.

After bootstrapping the project I worked in local-dev mode for a while focusing on just getting the grid layout and character placement working. I brushed off my A* and wrote a quick and dirty raytracing algorithm.

Important commands were:

# Bootstrap
$ npx amplify init
 # Add Hosting (to give me that handy http://amplify-dnd-20210225231527-hostingbucket-dev.s3-website-us-east-1.amazonaws.com/ url )
$ npx amplify add hosting
# Add auth
$ npx amplify add auth
# Add GraphQL
$ npx amplify add api
# Local Dev:
$ npx amplify push --y
# deploy
$ npx amplify publish

And following all of the CLI workflows which are very well thought out and descriptive.

So what is this doing? Behind the scenes the Amplify CLI is creating a bunch of nested cloudformation templates:

Screen Shot 2021-02-28 at 12.52.21 AM.png

You can see the root template towards the bottom followed (upward) with hosting, auth and the API. The API ends up being AppSync + a few DynamoDB Tables.

For my GraphQL Schema I decided on having a main Game model with a connected model for Rooms, Doors, Areas and Characters. Originally I thought about having a player create their own characters and then "joining" a game, but it ended up being easier permission-wise to have the DM create the characters and then simply share out links.

It's a very relational-way of doing things ๐Ÿฅฑ. Typically with DynamoDB you'd want to lean more towards a single table design with multiple entities in the same table, but the complexities that come from that when dynamically generating everything from some config files and adapting to frequent changes (as in a rapid prototyping setup)... it becomes much easier to keep everything separate. There's some interesting discussion on GitHub related to this and having done a lot of DynamoDB development over the last year... I get it

Setting up CD and a Custom Domain

Continuous Deployment

I had skipped the CD portion of the getting started tutorial, but decided I wanted to add it for branch autodetection and auto-disconnection. This is pretty easy to do. From the CLI you can run npx amplify console and it will open your browser to the console page. Add the GitHub repo and from there you can go to the App's General settings and edit them to turn on what you want. Easy!

Screen Shot 2021-02-28 at 11.15.16 AM.png

Custom Domain

I ended up deciding to add a custom domain to this to make it easier to access... default amplify URL is pretty gross (๐Ÿคฎ) http://amplify-dnd-20210225231527-hostingbucket-dev.s3-website-us-east-1.amazonaws.com/. With CD setup, this gets slightly better (๐Ÿคข): https://main.d1q1drpza83bun.amplifyapp.com/... but domains are "cheap". The Amplify Docs have a section on this. npx amplify console to bring the console back up and Domain Management and Add Domain and with minimal configuration and a few minutes of waiting, you're good to go. Announcing amplify-dnd.com

Tip, Tricks and Troubleshooting

  • Originally I created this with no auth in the GraphQL schema, and added it later ๐Ÿ˜ฑ
  • For some reason when I added CD and Custom Domain the SPA rewrite stopped working... that lead me to this docs page where I added </^[^.]+$|\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|ttf|map|json)$)([^.]+$)/> to redirect to /index.html and that fixed the issue for me.
  • I have a few things nested within the schema and had some issues with retrieving things that were too deep. You can configure this by running npx amplify codegen configure and then regenerating the definitions using npx amplify codegen. This is actually nice to do because you can select TypeScript instead of JavaScript
  • There weren't a lot of good examples for using TypeScript with react hooks for subscriptions. The best example I got was from this post. Which, for me, ended up being something like this:
useEffect(() => {
  fetchGame();
  try {
    const subscription = API.graphql(
      graphqlOperation(onUpdateGame, { input: { id: gameId } })
    ) as any;
    console.log(subscription);
    const subscribed = subscription.subscribe({
      next: (apiData: any) => {
        console.log(apiData);
        const apiGame = (apiData as any).value.data.onUpdateGame;
        if (apiGame.id === gameId) {
          restoreGame(apiGame);
        }
      },
      error: (err: any) => {
        console.error(err);
      },
    });
    return () => {
      subscribed.unsubscribe();
    };
  } catch (e) {
    console.error(e);
  }
}, []);

Which creates the subscription and sets it up to unsubscribe when the component is destroyed. During local development I used an instanceof type guard to narrow the type to an Observable, but when deployed to the amplifyaws domain and my custom domain it stopped working. The instanceof type guard worked for local development and via the s3 bucket. I wasn't able to figure that part out.

What's next?

I'm actually intending on using this, at least for a one-shot. I'd also like to add stuff like Items and possibly even NPC storefronts where players could buy/sell things. If I were to expand this on more of the serverless front... lambdas could offload a lot of the client processing (potentially). Definitely didn't have time to do that in these two weeks but it'd be easy to expand.

Would I use this for production? Probably not. I don't like the multi-table architecture and would want to do more with serverless, but maybe I'm just not well-versed enough with Amplify and/or AppSync at this point. I've only scratched the surface.

Would I use Amplify to do more rapid prototyping in the future? Absolutely. The best part of this project was the speed I could have a full-stack authorization-backed app deployed and ready to go in a matter of minutes.

One thing I wish I had time to try was the CDK module available for Amplify... but CDK Days is doing an RFP right now... ๐Ÿค”

What should I expand on with this post? I flew through some of the technical portion only because the Amplify portion was so trivially easy. Add a comment or hit me up on twitter @martzcodes ๐Ÿป

Interested in blogging?

If this post piqued your interest and you'd like to write some articles yourself, Hashnode is a great platform for it. You can join here

I might get some swag if you use my link

ย