
Loading, please wait...
This may take a while...
My Portfolio
My latest and greatest projects, ordered by date of completion
= Work in progress
= Notable project
= Has GitHub repository
Sellbot Task Force Pages, December 2021
The Sellbot Task Force Pages are a set of promotional webpages for a major content update to Toontown Rewritten.
Click to see more...
Sellbot Task Force Pages:
October 2021 - December 2021
Languages used:
- Java,
- HTML/CSS,
- Sass

Visit Sellbot Task Force Pages online:
About
The Sellbot Task Force Pages are a set of promotional webpages for the largest content update in Toontown Rewritten’s history. These pages were built in collaboration with the art team. Pages were laid out using the Play Framework in Java.
PreMatch, August 2021
PreMatch is a site that lets students at Andover High School see who is in their classes before school starts. It has since expanded to include an app and Discord bot centered around our chaotic schedule.
Click to see more...
PreMatch:
August 2018 - August 2021
Discover your classmates
Languages used:
- HTML,
- CSS,
- Ruby,
- Swift,
- JavaScript,
- Python,
- NoSQL

About
PreMatch is a site that allows students at Andover High School to quickly and easily enter their schedules, and find out which of their friends are in which of their classes or lunches. Since its first launch on August 22nd, 2018, it has expanded to include a Discord bot and a mobile app that comes bundled with a widget that gives any student quick access to their schedule. The widget looks like so:
The Problem
Behold - the 7+H schedule system of Andover High School:
It’s a chaotic 8-day rotation of 8 different blocks that vary in length combined with 4 lunch periods. One of these blocks is the H block, a block used for student enrichment, in which you can visit your teachers arbitrarily. When schedules are released one week before school starts, nearly every student at Andover High School goes in a frenzy, sending their schedule to everyone they know in order to see which of their friends are in which of their classes. This is a frantic, inefficient process that is never completely accurate and takes a long time.
Our Solution
When we, the creators of PreMatch, noticed how many students shared their schedules and the many platforms they used to share them, we decided that we had to make something that would make this process easier and more efficient, while still keeping sensitive information away from the prying eyes of the general public. Thus, PreMatch was born.
To use PreMatch, all you need to do is visit prematch.org, and log in with your school-assigned Google account.
Next, fill in your schedule by searching for your teachers. If you would like to, you may also add in your lunch period numbers for blocks C through G.
That’s it! You can now view your classes and your classmates!
Security
During the creation of PreMatch, security was always one of our top concerns. We wanted to make sure that only students could access the site, and nobody else. To do this, we implemented a Google sign-in integration, which requires you to be logged into a valid k12.andoverma.us Google account. Without being logged into an account, all you can see is the Home and About pages. In addition, we have implemented an “invisible” setting, allowing students to only make themselves visible to their classmates, meaning that they will not show up in searches, and their own schedule will be hidden to everyone but themselves. However, they will still be visible to their classmates, but only in their shared classes. For example, if Student A and Student B share a C block class, and Student B has marked themselves as invisible, Student A will be able to see that Student B is in their C block class, but they will not be able to see any other part of their schedule.
Credits
PreMatch was made by Michael Peng and Daniel Ivanovich, in the summer before their sophomore year. The site uses the Bulma framework and our backend is written in Flask. The Discord Bot was programmed in Ruby using discordrb, and the iOS app was made using Xcode. The site is hosted on Google’s App Engine, the database is hosted on Google’s Cloud Datastore, and the Discord bot is run on Google’s Compute Engine.
Catan Board Generator, April 2021
A website that utilizes an algorithmic analysis of the famously-complex board game, Catan, to generate random balanced (or very unbalanced) boards on demand.
Click to see more...
Catan Board Generator:
February 23rd 2021 - April 14th 2021
Languages used:
- Python,
- HTML,
- CSS,
- JavaScript

Visit Catan Board Generator online:
This project was made and submitted to the College Board as my “create task” for the AP Computer Science Principles course.
About Catan
This is a Catan board:
Clearly, there is quite a lot to a board. Every hexagonal tile can be one of 6 terrains: mountains, forests, fields, pastures, hills, and deserts. Every terrain hex type produces its own resource type, excluding the desert, which produces nothing. There are also number tiles for all non-desert terrain hexes. The terrain hexes and number tiles can be laid out in any way in any game, either randomly or, more typically, by the players’ design.
The Problem
Setting up a Catan board is a long process made even longer by desires to make a board that is fair to all players. In addition, the fairness of a Catan board itself is incredibly hard to analyze without guesswork, especially by human players within reasonable time. When a “balanced” layout is found, the difficulty of this process encourages players to use the same layout repeatedly across different games, decreasing variety and game enjoyment. How can Catan players make balanced boards quickly while having their boards differ from game to game to maintain variety?
The Solution: Catan Board Generator
Catan Board Generator is a website that generates random very balanced Catan board layouts. If the user desires for some twisted reason to play on a hilariously-unbalanced Catan board, Catan Board Generator can also generate random incredibly-unbalanced boards. The Python webserver uses an original algorithm to evaluate the balance of the complex Catan boards across 5 factors in order to generate and evaluate the outputted boards. Boards are scored between 0 and 1, with 0 being the most balanced (the closest boards will typically get is around 0.04) and 1 being the most unbalanced (the closest boards will typically get is around 0.62). This algorithm is broken down in detail below.
Features
- Built from the ground-up following the principles of object-oriented design
- Thorough board balance evaluation algorithm built upon 5 factors:
- Resource distribution
- Resource clustering
- Probability clustering
- Probability distribution per resource
- Probability distribution on board
- Test-driven development: thorough test system to evaluate each factor on several known layouts
- Flask-based web server with minimalistic interface
- Clear, vector-graphic interface of generated board layout
Deep Dive: Board Balance Scoring Algorithm
The board balance evaluation algorithm is built upon 5 distinct factors that are combined into a final score between 0 and 1, where 0 represents the most balanced board and 1 represents the least balanced board. Typical ranges of this final score are detailed above.
Resource Distribution
When measuring distribution across the board, the symmetry of the shape of a Catan board was utilized. Three lines through the center of the board that do not cross any valid settlement locations can be drawn as follows:
We will refer to these lines as the “dividing lines.” These were used in the resource distribution scoring algorithm. For each dividing line, the algorithm examines all possible settlement positions and counts the frequency of the 1-3 terrain types accessible at that position. For each resource type, the frequencies on each side of the line are summed, then the sums on one side of the dividing line are subtracted from the sum on the other side of the line. The differences are squared, then these squares are summed.
For example, here is what the process would look like for the horizontal dividing line for forest terrain hexes:
Here are what the resource distribution scores would look like for various boards, illustrating how the algorithm properly scores resource distribution for balance:
The lower the score, the more balanced the algorithm has deemed the board to be.
Probability Distribution on Board
This algorithm is similar to the resource distribution algorithm, using the same three dividing lines. For each dividing line, this algorithm also considers every possible settlement location, summing the relative likelihoods of the 0-3 number tiles accessible from that location. Again, the scores are summed for each side of the dividing line, then the difference between the halves is squared and these squares are summed.
Here are what the probability distribution scores would look like for various boards, illustrating how the algorithm properly scores probability distribution for balance:
The lower the score, the more balanced the algorithm has deemed the board to be.
Resource Clustering
Resource clustering is a far simpler algorithm, adding points for every edge that two resources of identical types share. Here are what the resource clustering scores would look like for various boards, illustrating how the algorithm properly scores resource clustering for balance:
Again, the lower the score, the more balanced the algorithm has deemed the board to be.
Probability Clustering
Probability clustering is analyzed by a similar algorithm, adding points for every edge shared by two terrain hexes with the same number tile. Here are what the probability clustering scores would look like for various boards, illustrating how the algorithm properly scores probability clustering for balance:
You get the drill, lower is better.
Probability Distribution Per Resource
The fifth and final factor used to score a board’s balance analyzes the probability distribution per resource. This algorithm was built to ensure resources have a total probability of paying out proportional to their presence on the board. In short, it takes the difference between the expected and actual payout probabilities for each resource type, squares the differences, then sums the squares.
Here are what the probability distribution per resource scores would look like for various boards, illustrating how the algorithm properly scores probability distribution for balance:
As always, lower is better.
Putting It All Together
These 5 aforementioned factors are scaled down between 0 and 1 using their highest expected individual scores, then these scaled-down results are averaged with uniform weighting to provide the final 0-to-1 score.
When a user asks the server to generate a balanced (or unbalanced) board, the server generates several thousand random boards, scores them, and shows the user the most (or least) balanced board that it generated. Thus, although not every board generated will be the theoretically most-balanced board possible, they will all be fairly close to this bound while continuing to vary with every generation, letting future games still be interesting.
Methodology inspired by and images from Board Game Analysis
Screenshots
The home page of the interface
The page displays a loading animation as it generates a balanced board
The user is redirected to a page displaying the generated board and its associated balance score. This board scored 0.0442, meaning it is very well-balanced. A cursory glance makes it clear why this is the case - there are several equally-good starting positions available.
The result if the user asked for an unbalanced board. This board scored 0.6126, meaning it is very unbalanced. It's clear why this is - terrain types are clustered together, as are the best number tiles.
The pages with the generated boards are populated via GET parameters, which would allow a player to bookmark/save a board they like or share the generated board with other players simply via URL.
I hope to host a demo of this project in the near future on some sort of free hosting service, so that I and other Catan players may use it in their game nights. I may end up turning the demo into a static site that chooses from a pre-generated, large list of potential boards. This would be less-than-ideal and a waste of the capabilities of the Python backend, but easy to find free, good-quality hosting for, which will likely be the dominating factor. The required changes to the source code will be made in a separate branch when this happens.
Bananagrams Online, September 2020
An original, online, multiplayer implementation of the world's best boardless Scrabble game.
Click to see more...
Bananagrams Online:
August 21st, 2020 - September 25th, 2020
Languages used:
- HTML,
- CSS,
- JavaScript

Visit Bananagrams Online online:
About
Bananagrams Online is a web-based online multiplayer recreation of Bananagrams. Users are able to create lobbies with custom names and sizes. The video demonstrates the process of creating and joining a game lobby through the web interface, starting the game, and playing the game to completion. This is done using two browser windows, functioning just as two different users on different computers would. Players play the game by dragging and dropping their tiles on a tiled grid system. When they form a valid crossword, they are able to peel, and all players draw a tile. When there are no tiles left and a player finishes their crossword, their words are read and validated against the 2019 Scrabble Dictionary.
This project began as a summer project for my AP Computer Science Principles class (2020-21 school year), but its development has continued beyond that scope.
Inspiration
My siblings and I have always enjoyed playing Bananagrams together and with our relatives. Considering that my sister is in New York, my brother is in Washington, D.C., and I am in Massachusetts, it is not easy for us to continue our tradition or spend much time together. For this reason, I looked into ways we could play Bananagrams online. Disappointingly, I only found unsatisfactory ideas for how to adjust the rules of the game to play over video calls. These rulesets involved each player using their own set of tiles and their own smaller bunch to play the game. Such a game would hinder the frustrating but essential aspect of the game that is drawing a hard-to-use tile (or the joy of hearing a player’s reaction to a disappointing draw). If each player uses a reduced bunch, it would be unlikely that every Q, V, X, and Z would be included, and one player may have a much easier bunch than their competitors. For this reason, a complete implementation of the game over the internet would allow the game to be played from different parts of the country while preserving the full experience of the game (minus the oddly-satisfying feel of the game’s tiles, a significant selling point for me).
The Game
Bananagrams—the board game—is essentially Scrabble without a board, where each of the 2-8 players races against their opponents to build a crossword freely until there are no tiles left. Each player builds their own crossword in the shape of their choosing, using tiles they draw from a shared pile (the “bunch”). The amount of tiles each player begins with varies on the number of players involved (the game is played with two to eight players), but every player starts with an equal amount of tiles, kept facedown so that nobody sees their letters beforehand. The game begins when a player shouts “split,” and the players then flip over all of their tiles, revealing all their letters to themselves. From that moment on is a race to construct a valid crossword that uses all of a player’s tiles. When a player manages to complete their crossword, they shout “peel,” and all players draw one tile from the bunch. Then, the process repeats as players attempt to integrate their latest piece into their crosswords. Crosswords may be edited or entirely reconstructed at any point in the game: sometimes drawing a letter such as an X or a Z will result in a player demolishing half of their board and reconstructing an entirely different section to implement their piece. At any point during the game, a player may choose to “dump,” meaning that they return a piece of their choosing to the bunch and draw three new tiles in exchange. This process may not be done if there are fewer than three tiles left in the bunch. The game ends when a player has completed their crossword and there are not enough letters remaining in the bunch for every player to be able to draw one. At that point, the player shouts “bananas,” and the game stops as all of the potential victor’s opponents review the board, checking for illegal or misspelled words. If the board is valid, the board’s creator wins the game. If an invalid word is found, the board’s creator is disqualified from the match and the remaining players continue racing to be the next to finish their board.
Project Design Document
Before beginning the development of this project, I wrote up my idea and design goals. I also laid out some milestones to guide my development. The document may be found here, and will be updated as more progress is made.
Features
- Customizable game lobbies
- Lobby browser
- Lobby host system
- Custom player names
- Full, accurate bunch
- Seeded bunch shuffling ensures all players start with the same bunch
- Drawing occurs in a way that ensures that every tile is used once, and only once, in every game
- Automatic detection of valid crossword shapes
- Automatic and efficient validation of all words in final crosswords against official Scrabble dictionary
- Ability to pan view or move all pieces at once means players will never run out of space, even without an infinite play area
- Persistent websocket system (built off of Socket.IO)
Tools, Languages, and Technologies Used
Back end:
- JavaScript
Front end:
- HTML
- CSS
- JavaScript
- FontAwesome - a font and icon toolkit for the web
- Materialize - a responsive front-end framework
Demo
I have not yet begun to look into hosting options for Bananagrams Online - there is still a bit of development to be completed before it will be ready. In the meantime, this one-minute video, made as a deliverable for my course summer work, demonstrates much of the game’s functionality and explains how it is played.
The one-minute video demo delivered as part of my AP CSP summer project
Notable Algorithms
Crossword Validation Algorithms:
// gridTiles is the Grid's 2D array of GridTiles
function isValidShape(gridTiles) {
let visited = [];
for (let i = 0; i < gridTiles.length; i++) {
let arr = [];
for (let j = 0; j < gridTiles[i].length; j++)
arr.push(false);
visited.push(arr);
}
// Count how many blobs there are in the grid. There should only be 1 in a valid crossword.
let checkQueue = new Queue(), blobCount = 0;
for (let i = 0; i < gridTiles.length; i++) {
for (let j = 0; j < gridTiles[i].length; j++) {
if (gridTiles[i][j].occupied && !visited[i][j]) {
checkQueue.enqueue(gridTiles[i][j]);
blobCount++;
while (!checkQueue.isEmpty()) {
let tile = checkQueue.dequeue();
if (!visited[tile.row][tile.col]) {
visited[tile.row][tile.col] = true;
if (tile.occupied) {
checkQueue.enqueue(gridTiles[tile.row - 1][tile.col]);
checkQueue.enqueue(gridTiles[tile.row + 1][tile.col]);
checkQueue.enqueue(gridTiles[tile.row][tile.col - 1]);
checkQueue.enqueue(gridTiles[tile.row][tile.col + 1]);
}
}
}
} else
visited[i][j] = true;
}
}
return blobCount === 1;
}
My crossword shape validation algorithm. It is a modified implementation of a flood fill algorithm that calculates the number of disjoint "blobs" in the 2D array of `GridTile`s. For a crossword to have a valid *shape* (words are not validated yet), there should only be one such blob.
class Word {
constructor(gridTiles) {
this.text = '';
this.gridTiles = gridTiles;
this.constructText();
}
constructText() {
let word = '';
this.gridTiles.forEach((tile) => {
word += tile.containedTile.letter;
});
this.text = word;
}
}
// Finds all the Words in the crossword, if it is valid. Returns them as an array of Words.
function findAllWords(grid) {
if (!isValidShape(grid.tiles))
return null;
let tiles = [...grid.occupiedTiles]; // Make a copy of occupiedTiles
let words = [];
// First, find all VERTICAL words
// Sort from top to bottom, then left to right
tiles.sort((a, b) => {
let colDiff = a.col - b.col;
return colDiff !== 0 ? colDiff : a.row - b.row;
});
let checkQueue = new Queue(), visited = [];
for (let i = 0; i < tiles.length; i++)
visited.push(false);
// For every tile, check all tiles below it and form a Word if there is one (letters >= 2)
for (let i = 0; i < tiles.length; i++) {
if (!visited[i]) {
checkQueue.enqueue(tiles[i]);
let word = [];
while (!checkQueue.isEmpty()) {
let tile = checkQueue.dequeue(), tileIndex = tiles.indexOf(tile);
if (!visited[tileIndex]) {
word.push(tile);
visited[tileIndex] = true;
if (tile.col !== grid.numCols - 1)
if (grid.tiles[tile.row][tile.col + 1].occupied)
checkQueue.enqueue(grid.tiles[tile.row][tile.col + 1]);
}
}
if (word.length >= 2)
words.push(new Word(word));
}
}
// Now check HORIZONTAL words and append them to the array as well
// Sort from left to right, then top to bottom
tiles.sort((a, b) => {
let colDiff = a.row - b.row;
return colDiff !== 0 ? colDiff : a.col - b.col;
});
// Reset visited array
for (let i = 0; i < visited.length; i++)
visited[i] = false;
// For every tile, check all tiles to the right of it and form a Word if there is one (letters >= 2)
for (let i = 0; i < tiles.length; i++) {
if (!visited[i]) {
checkQueue.enqueue(tiles[i]);
let word = [];
while (!checkQueue.isEmpty()) {
let tile = checkQueue.dequeue(), tileIndex = tiles.indexOf(tile);
if (!visited[tileIndex]) {
word.push(tile);
visited[tileIndex] = true;
if (tile.row !== grid.numRows - 1)
if (grid.tiles[tile.row + 1][tile.col].occupied)
checkQueue.enqueue(grid.tiles[tile.row + 1][tile.col]);
}
}
if (word.length >= 2)
words.push(new Word(word));
}
}
return words;
}
My crossword word extraction algorithm. It implements 2 modified flood fills, first only flooding vertically and recognizing any columns larger than 2 tiles in height as `Word`s. Then, it floods horizontally, recognizing any rows larger than 2 columns in width as `Word`s. All extracted `Word`s are added to an array, which is returned.
// Returns the index of the word in the Dictionary if it is in the dictionary, otherwise returns -1
function binarySearch(word) {
let low = 0, high = dictionary.length - 1, mid = Math.floor((high + low) / 2);
while (dictionary[mid] !== word && low < high) {
let comparison = word.localeCompare(dictionary[mid]);
if (comparison < 0)
high = mid - 1;
else
low = mid + 1;
mid = Math.floor((high + low) / 2);
}
return (dictionary[mid] === word) ? mid : -1;
}
The algorithm that checks if a word is in the scrabble dictionary - an implementation of a basic binary search.
Bunch Algorithms:
function shuffle() {
// Swap 2 random tiles, 5000 times
for (let shuffleCount = 1; shuffleCount <= 5000; shuffleCount++) {
let tile1 = this.pickRandomTile(), tile2 = this.pickRandomTile();
let temp = this.tiles[tile1];
this.tiles[tile1] = this.tiles[tile2];
this.tiles[tile2] = temp;
}
}
function pickRandomTile() {
return Math.floor(this.rng() * this.tiles.length); // Generates a random index in the array
}
The that uses seeded random number generation to shuffle the `Bunch`.
// Draws tiles from the bunch in a rotating manner, determined by playerOrder (1-indexed). For example:
// say there is a 4-player game, with the first 10 tiles in the bunch being [A, B, C, D, E, F, G, H, I, J].
// If each player were to draw 2 tiles, they would be dealt from the front, with the first going to to the player with
// playerOrder of 1, the second going to the player with playerOrder of 2, etc., and then the cycle would repeat.
// Thus, player 1 would get tiles A and E, player 2 would get B and F, 3 would get C and G, and 4 would get D and H.
// This example explains how the game's drawing system will work and the significance of each player's playerOrder.
//
// Although the tiles will be drawn in this manner, this method only draws for ONE player. The bunches are all shuffled
// based off of a seeded RNG algorithm, meaning that every player in the game will end up having the same bunch. Thus,
// this method returns a list of the tiles THIS player will receive, but still removes all the tiles the OTHER players
// will have received from the draw. Using the example from the last paragraph, if the player with playerOrder 2 ran
// this method to find which 2 tiles they would receive, they would receive [B, F] as a return value. However, after
// the method has finished executing, the bunch will begin with [I, J, ...], as ALL of the first 8 tiles have been removed
// by the 4 players, not only the 2 removed by THIS player. Thus, the bunches will all remain synchronized and less
// networking must be done. Isn't that neat? :)
//
// Parameters:
// numberToDraw - how many tiles will be drawn in this fashion (in the given example this would be 2)
// playerOrder - the drawing player's drawing order (1-indexed)
// playerCount - the number of players in the game
//
// Returns:
// 1) null, if there are not enough tiles left for each player in the game to draw numberToDraw tiles
// 2) An array of Tiles that thi player will draw. The first tile they will draw will be at index 0.
function drawTilesAsGroup(numberToDraw, playerOrder, playerCount) {
// The total number of tiles to be drawn from the bunch by the group
let totalTilesToBeDrawn = numberToDraw * playerCount;
if (totalTilesToBeDrawn > this.tiles.length)
return null;
let drawnTiles = [];
// We extract Tiles from this.tiles and add only those that will be drawn by this player to drawnTiles
// We always only work with the FIRST element of the array because otherwise shifting indices will mess things up
for (let tileNum = 1; tileNum <= totalTilesToBeDrawn; tileNum++) {
let tile = this.tiles.shift(); // Remove the first Tile from the Bunch
// Will this Tile go to THIS player? (Modulo on right side to make the last player work)
if(tileNum % playerCount === playerOrder % playerCount)
drawnTiles.push(tile);
}
return drawnTiles;
}
The algorithm that distributes tiles from the `Bunch` as if it were a physical game, ensuring each tile is used only once across all players in the current game. See the comment for a detailed explanation.
TrelloTown:
September 2019 - June 2020
Languages used:
- Python,
- MySQL,
- JavaScript

About
TrelloTown is a bot I made for the Toontown Rewritten Team’s various internal Discord servers and Trello boards. Discord is a free text and voice chat service that is organized into “servers,” each of which contains many text and voice “channels” - in many ways, a blend of Slack and TeamSpeak. Trello, on the other hand, is a web-based Kanban-style list-making application. The Toontown Team, as with other groups with many members, departments, and tasks, uses a variety of channels (including Discord and Trello) to communicate and organize work. Thus, TrelloTown was designed as one of the ways to merge methods of communication and organization.
TrelloTown is a Discord Bot that utilizes Discord Webhooks, the Trello REST API, and Trello Webhooks to log updates to Trello boards to Discord servers. Through Discord direct messages, Trello authorization is performed and tracking is enabled and configured. Then, Trello webhooks are created for tracked objects (from entire boards to specific lists and cards). When changes are made to a tracked object, its Trello webhook sends a message to TrelloTown’s server detailing the alteration. The TrelloTown server then extracts the relevant data, queries its database to obtain a list of all Discord webhooks associated with that tracked Trello objet, and sends a well-formatted “embed” message to each webhook. This also means that TrelloTown is able to log changes to servers that its Discord bot account is not a part of, but it will verify that the initiator of the tracking is a member of that server before beginning to do so.
The code base for this project is private.
Tools, Languages, and Technologies Used
- Python
- discord.py - an async-ready API wrapper for Discord and Discord bots
- dhooks - a Python library for interacting with Discord webhooks
- AIOHTTP - an asynchronous HTTP client and server for asyncio
- aiohttp-jinja2 - a jinja2 template renderer for AIOHTTP
- mysql-connector-python - a Python MySQL Driver
- py-trello - a Python wrapper around the Trello API
- gunicorn - a Python WSGI HTTP server for UNIX
- HTML, CSS, and JavaScript front end:
- jQuery
- Trello’s client.js - Trello’s JavaScript library for working with the Trello REST API through web applications
Authorization
Before Trello object tracking may be configured, a user must first have their Discord account associated with a Trello account. This is done through the authorization process. As can be seen in the following screenshot, when an unauthorized user attempts to begin tracking in direct messages (hereafter “DMs”), TrelloTown’s Discord bot informs them that they are unauthorized. The user must then enter the authorize
command (denoted by the prefix(es) set within the server(s) that the user and TrelloTown share, in this case $
). This results in TrelloTown prompting the user to visit a custom-generated link (one that includes the user’s Discord ID as a GET parameter).
When the user opens this link, they are taken to the following sign in page:
Clicking the “Link Account” button opens a popup window with a Trello log in page, if the user is not logged into Trello already. After signing in, they prompted with the following confirmation page:
Once they press “authorize,” a Trello API key for the user’s account is generated. The popup window closes, the API key is passed to the authentication page, and the user is redirected to a loading page as their accounts are being linked:
After an association between the user’s Discord ID and Trello API key is created (or updated if one already exists) in TrelloTown’s database, the user is redirected to this final confirmation screen that tells them to close the page and return to Discord:
Now, when the user tries to initiate tracking, they are no longer prompted with an authentication error - at least until they supply a valid webhook. When a valid webhook is supplied, TrelloTown checks the server in which the webhook will output its messages. If the user is an administrator, they are allowed to continue with the tracking initialization process but if they have not already done so, are reminded to set up their server with the setup
command (not pictured). However, if the user is not an administrator and the server has not been set up (pictured below), the user is informed that they do not have permission to initialize tracking and should consider asking administrators to set up their server. If the user is not an administrator and the server has been setup, then the user would simply be informed that they do not have permission to set up tracking to their provided webhook (not pictured).
Server Setup
Now, if a server administrator wanted to set up their server to allow tracking by non-administrators - that is to say, if they wanted to configure their server so that holders of a particular Discord role had permission to enable tracking, even if they do not have full administrator privileges - they would begin by running the setup
command in their server.
As demonstrated above, the administrator would be guided to their DMs, where they would see the following prompt:
The icons underneath this message are Discord reactions - by clicking on a reaction, a user can add the same reaction of their own. Thus, as TrelloTown’s bot has reacted to its own message twice - once with a check mark and once with an X, the user can use these icons as buttons to easily communicate with the bot. This is one of the many features of TrelloTown that simplify the process of using the bot, and these reaction buttons will be seen quite often throughout the rest of the screenshots on this page.
The administrator can then go through the setup process, choosing a role from a paginated list of their server’s roles (pagination not visible because there are fewer than 10 roles on the demo server) and finalizing their choice. Pressing an X at any time will cancel the process. Upon confirming their selected role, TrelloTown’s database will update, creating an association between the server’s Discord ID and the configured role’s ID. If such an association already exists with a different role’s ID, that association will be updated with the new role’s ID.
Now that the database has been updated, the server’s set up is complete. All future TrelloTown tracking commands for this server will cause TrelloTown to validate that its associated role still exists - if the configured role has been deleted, so will the row in the database, and the server will need to be set up again by an administrator.
Tracking a Trello Object
Before Trello tracking may be set up, you first must create a Discord webhook for a specific text channel in your server. This can be done by administrators under the server settings page. For example, here I have created a webhook named “Captain Hook” for the text channel trello-announcements
:
Copy the webhook URL by pressing the copy button, then either use it to set up tracking yourself or share the URL with another user with permission to set up tracking. Do NOT share the webhook URL with users or applications that you do not trust - having possession of a webhook URL allows you to send messages into the webhook’s channel with HTTP requests, even if you are not a member of its server.
Now that you have your webhook URL, run the track command. Once again, do NOT supply the track command with its webhook URL argument in a server’s publicly-visible text channel. If you run the the track command in a server, only call it as track
, as shown below:
However, it is recommended that you rather DM TrelloTown’s bot and call the track command in the format {prefix}track {webhook URL}
, as shown below. If your webhook is of a valid URL format, you will be prompted with another set of reaction “buttons” - this time to select if you would like to enable tracking of a Board,List, or Card. From then on, you will be prompted with paginated, numbered lists of boards, the lists within the board you chose, if appicable, and finally the cards within the list you chose, if applicable. Pagination is not shown below or in any of the screenshots from this example, as more than 10 boards, lists, or cards must be present in a menu for pagination occur. However, pagination is demoed in this section.
If you make a mistake, you can return to the previous selection menu by sending back
(no prefix required).
Once you have selected the board, list, or card that you want to track, you will have the chance to confirm your selection, once again using reaction “buttons.” If you confirm your choice, TrelloTown will then verify the webhook URL you provided. It will check for the following:
- Is the webhook URL in a valid format? (Checked when the command is first received)
- Is the webhook URL valid? That is to say, does it point to a text channel in a Discord server?
- Is the user who called the command authorized to set up tracking in this server? To answer this question, TrelloTown iterates over the following steps:
- Is the user part of this server? If not, they are unauthorized.
- Is the user an administrator? If so, they are authorized.
- Has the server been set up? If not, there is no way that this user can be considered authorized.
- Does the user possess the role configured during the set up process? If so they are authorized.
- If the user has reached this point in the control flow, the user is unauthorized.
- If the URL is valid and the user is authorized, send a test message. Does the test message go through successfully?
If the webhook URL is deemed to be valid, the user is deemed to be authorized, and the test message went through successfully, then the user will see the highlighted confirmation messages:
The test message follows the format shown below. In this example, it was sent to the #trello-announcements
text channel.
And with that, tracking has been set up for your board, list, or card! Future changes will be logged using the webhook. A single Trello object can be linked to any number of webhooks and servers, but TrelloTown will prevent you from configuring an object to tracked twice in the same channel in the same server, even across different webhook URLs!.
Tracking Demo
After having set up tracking as seen above (tracking the list Track this List!
in the board TrelloTown Test
), making changes to the list or any cards inside of it will result in a new changelog in the text channel trello-announcements
. For example, if I were to add a card to the list, a message in the following format would be sent:
Notice how the message contains the following:
- A description of the action alongside the names of the list and board (text above the embed)
- The author of the Trello change (
Daniel Ivanovich
)- The author’s Trello username (
ivanov1ch
) - The author’s Trello profile picture, if it exists (top left corner)
- The author’s Trello username (
- The title of the card (
What's under the cloud?
)- The title is a link to the card on Trello
- The card’s cover image, if it exists (top right)
- The textual description of the card (blurred)
- The names and usernames of the members of the Trello board assigned to the card (
Daniel Ivanovich (ivanov1ch)
) - Any and all card checklists
- The names and statuses of each item on each checklist (
Thing 1 (Incomplete)
)
- The names and statuses of each item on each checklist (
- Any and all labels assigned to the card (
Label 1
) - The card’s due date and completion status (
Monday, April 20, 2020, at 20:20 UTC (Incomplete)
) - The time stamp of the change to the Trello item (bottom left)
Although the exact contents of each changelog will vary according to the object and action types (for example, editing a particular section of a card, creating a new list, or moving a card across lists), this example provides a good demonstration of what can be expected.
For example, I were to remove this new card from its list by archiving it, the following would be logged:
You may notice that, as only the information most relevant to the action is logged, this message is far shorter and only contains:
- A description of the action alongside the names of the list and board (text above the embed)
- The author of the Trello change (
Daniel Ivanovich
)- The author’s Trello username (
ivanov1ch
) - The author’s Trello profile picture, if it exists (top left corner)
- The author’s Trello username (
- The ID of the card
- The specifics of the card’s removal (mentioning that it was
archived
, not deleted) - The time stamp of the change to the Trello item (bottom left)
In this way, changes to Trello objects will be logged through webhooks. If a tracked object is deleted (not archived), TrelloTown will log its deletion, then automatically stop tracking the object (the Trello webhook will be delete alongside the object).
Tracking Interface Pagination Demo
As demonstrated by the following screenshot, if, at any point during the track command setup process, a menu contains more items than a configurable maximum page size (10 by default), users can use next
and previous
to navigate a paginated menu to find their desired Trello objects.
TrelloTown also supports other commands, such as deleting existing tracking associations, but these, for the sake of brevity in this already-lengthy description, will be left out.
Nginx IP Updater, September 2019
A Python-based application that detects when the inet of a device changes and reconfigures its Nginx server accordingly.
Click to see more...
Nginx IP Updater:
July 28th - September 3rd, 2019
Language used:
- Python

Visit Nginx IP Updater online:
About
Nginx (pronounced “engine x”) is an open-source, easy-to-use HTTP server and response proxy that can be used to create a variety of web servers. Nginx IP Updater is a Python-based application that automatically detects when the inet IP address of a device changes, and updates its Nginx configurations accordingly. It was developed for an internal Energize Andover server, based in Andover High School, that hosts several student-built projects using Nginx. More specifically, I developed this to resolve an issue where, seemingly randomly, the address assigned to our server on Andover High School’s internal network would change. As our Nginx configurations host on a user-provided address, in this case our inet IP address, these changes make our applications inaccessible without notice.
Upon these IP changes, Nginx IP Updater corrects Nginx configurations and restarts the server to apply them. Furthermore, it can be configured to email a list of recipients, informing them of the change in addresses.
Features
- Automatic detection of IP address changes
- Fully configurable - all of the following may be configured in
config.py
:- Path to the Nginx configuration folder
- List of files within the Nginx configuration folder to update on IP address change
- Network interface of inet IP address broadcast
- Configurable email notifications alert server administrators of the changes. All of the following may be configured in
config.py
:- SMTP server
- SMTP port
- Server email address (sender email address)
- Server email password (sender email password)
- List of recipient email addresses
- Email subject
Installation and Configuration
- Clone the repository
- Edit
config.py
according to the comments - Run
git update-index --skip-worktree config.py
in the root directory to ensure that the confidential information inside your configuration is not committed or pushed. - Run
main.py
as an administrator
Nginx Indexer, September 2019
A Python-based application that reads Nginx configuration files and hosts a directory webpage displaying all applications running on the Nginx server.
Click to see more...
Nginx Indexer:
March 15th - September 3rd, 2019
Languages used:
- Python,
- HTML,
- CSS,
- JavaScript

About
Nginx (pronounced “engine x”) is an open-source, easy-to-use HTTP server and response proxy that can be used to create a variety of web servers. Nginx Indexer is a Python-based application that reads through the configuration files of an Nginx installation and detects which local ports are forwarded to each location of the server. The program than queries each location to retrieve the name of the pages hosted there. This data is compiled and displayed in a Flask-based web server in the form of a directory to all applications running on the server. Thus, this project is essentially an automatic homepage for an Nginx server with multiple applications on it.
Features
- Automatic indexing of a configurable set of Nginx configuration files
- Configurable startup delay allows applications hosted by the server to start up before the indexing process begins
- Automatic application title retrieval by querying the endpoints scraped from configuration files
- Flask-based configurable web server to host the generated server home page
How It Works
Nginx Indexer was developed for an internal Energize Andover server, based in Andover High School, that hosts several student-built projects using Nginx. On this server, the Nginx-Indexer-created webpage sits at the base URL (/
) and the AHS Heatmap is hosted at the /heatmap/
endpoint (other projects, such as the BACnet AHS Electric Gauge are hosted at other endpoints, but this heatmap example simplifies the explanation). As it is starting up, the Nginx Indexer would thus obtain that, at the endpoint /
, the application running at localhost:5000
is displayed (Nginx Indexer), and the application running at localhost:8000
(AHS Heatmap) is displayed at the endpoint /heatmap/
. The application would then query {device IP or URL}/
and {device IP or URL}/heatmap/
to obtain the titles of the applications running at each endpoint - in this case, Energize Andover Index
and AHS Heatmaps
, respectively. After collecting all this data, Nginx Indexer starts up a Flask web server and passes the endpoint and title data to the template for its home page. The home page (located at {device IP or URL}/
) would generate links and titles to form a home page for the server as a whole, visible in the screenshot in the next section.
Screenshots
A screenshot of the webpage created by Nginx Indexer on our server, under the conditions described above
Installation and Configuration
- Clone this repository
- Install the requirements (you can use
pip install -r requirements.txt
to automatically install all of them) - Edit
CONFIGURATION_PATHS
inmain.py
like so: Add the paths to every Nginx configuration file that initializes a location you would like indexed (usingos.path.join()
is recommended to guarantee the paths are correct) - Run
app.py
and visitlocalhost:5000
. The site should be live. - If you would like to host this page on your own Nginx server’s root, I recommend using
gunicorn
(this package should automatically be installed fromrequirements.txt
).
AHS Heatmap, August 2019
Reads sensors in Andover High School and, given any number of SVG floor plans, hosts a website containing interactive versions of these plans with their rooms colored according to their temperatures or carbon dioxide levels.
Click to see more...
AHS Heatmap:
August 2018 - August 2019
Languages used:
- Python,
- HTML,
- CSS,
- JavaScript

About
This program reads from sensors around Andover High School (AHS) and, from any amount of given SVG floor plans, hosts a website containing interactive versions of these floor plans, in which rooms are colored according to their temperature or carbon dioxide values. These maps provide a simple, quick, and comprehensible method of locating rooms, wings, or floors with ventilation problems. It can work with any amount of floor plans, given that they meet the required format (see below).
Features
- Quickly fetches data from all sensors
- Usable on any map provided in SVG format
- Easy-to-use, understandable, and interactive maps
- Automatically regenerates and updates maps
- Map updating process ensures no downtime for viewers
- Error catching in the case of server failure
- Easy configuration of the definition of pure red, blue, and green values
- Colors corresponding to sensor readings belong to a generated even gradient
Related Blog Posts
- How to Extract the Coordinates of Text in a PDF
- AHS Heatmap Presentation Recap
- OpenCV and AHS Heatmap | Automatically Finding Rooms in an Image (Part 1)
- OpenCV and AHS Heatmap | Working with SVGs (Part 2)
- OpenCV and AHS Heatmap | Autonomous Room Detection in Floor Plans With OpenCV (Part 3)
The Process
See the blog posts “OpenCV and AHS Heatmap | Automatically Finding Rooms in an Image (Part 1),” “OpenCV and AHS Heatmap | Working with SVGs (Part 2),” and “OpenCV and AHS Heatmap | Autonomous Room Detection in Floor Plans With OpenCV (Part 3)” for an in-depth explanation of the computer vision behind AHS Heatmap, used to automatically generate an interactive map from any SVG provided.
The following is the slideshow about the AHS Heatmap and how it works, presented at a town meeting in the August 2019 (related blog post).
Demo
This project is hosted live at heatmap.energizeandover.com! Check it out!
Floor Plan Requirements
This project supports any number of heatmaps, granted they meet the following criteria:
- They are in SVG format
- They are placed inside the
/static/svg_and_conversions
folder (link) - They are named
Andover-HS-level-{level}.svg
, where{level}
is replaced with a number representing the level of the building mapped, with1
being the field house and4
being the floor containing rooms 301 and up - The room numbers are located fully inside their room
- Rooms do not have holes in their walls
An example of a properly formatted floor plan can be seen here:
Installation
- Clone this repository
- (Optional): Create a python virtual enviornment for this project
- Install the project’s dependencies (see “Dependencies”)
- Install Inkscape and add it to your system’s PATH (you should be able to open Inkscape by opening a terminal and running
inkscape
from any directory) - Ensure you have an internet connection and run
main.py
For more detailed instructions, see “Initialization”
See “Security Notice” to learn more
Dependencies
To run this project on a machine, you must first install its many required packages. See requirements.txt for a list of required packages and their versions.
To quickly install all requirements, use the command pip install -r requirements.txt
while in the AHS-Heatmap
directory, or simply run install_packages.sh to have the installation process done automatically.
Initialization
Cloning
First, clone this repository. Either use GitHub Desktop, download the repository as a ZIP and extract, or run git clone https://github.com/Energize-Andover/AHS-Heatmap.git
in the command line. Any of these methods will bring a copy of the repository into a local folder, named AHS-Heatmap
.
Inkscape
An external requirement of the Heatmap is Inkscape, a free universal SVG editor. It is used by the program to convert SVG files into PDF and PNG format for further map analysis. Thus, the next step is to visit inkscape.org, open the Download
page, and download and install inkscape for your operating system.
Python Packages
The next step is to install the required packages. Navigate to the AHS-Heatmap
directory in your command line. Run pip install -r requirements.txt
or, if on a UNIX device, run install_packages.sh
to install all requirements.
Configuration + Index Updating
This is the most important step. By this point, your local installation of AHS Heatmap is in a runnable state. To ensure proper operation, edit config.py
’s global configuration variables to your liking. See the comments (text beginning with #
s) to understand what each variable is and what it does. Do not delete any of the variables - doing so will stop the Heatmap from running.
Finally, please ensure you keep your machine-specific configurations out of the repository by running git update-index --skip-worktree config.py
in the AHS-Heatmap
directory. Doing so will stop local updates of config.py
from being tracked, committed, or pushed.
Security Notice
If you seek to run this project on a machine securely, do not simply run wsgi.py
or app.py
(both start the app using Flask). The app should instead be run by using gunicorn to run wsgi.py
, a more secure and reliable long-term method of hosting a web server.
Credits
This project was made by Daniel Ivanovich for the Energize Andover Program between the falls of 2018 and 2019. The buildingEnergyApi was used to request data from the sensors. jQuery and Bulma were used to create the web interface.
This project is licensed under the MIT License.
Jekyll + Liquid Date Converters, July 2019
Plugins for Jekyll, a web development tool, that allows you to output the dates of posts from the .md file in different formats
Click to see more...
Jekyll + Liquid Date Converters:
July 2019
Language used:
- Ruby

Visit Jekyll + Liquid Date Converters online:
About
A collection of Jekyll plugins related to the conversion of Jekyll posts’ dates to other formats. This repository will be updated as I develop more plugins for my personal projects.
Plugin Installation
To install any plugins, follow the following steps:
1) Clone this repository or download it as a ZIP archive and extract the Jekyll-Liquid-Convert-Dates
folder
2) Find the .rb
files for each of the plugins you wish to install
3) Copy the .rb
files into the _plugins
directory of your Jekyll project. If the _plugins
directory does not exist, make it.
Example and Setup
See the README.md file of the GitHub repository.