Skip to content

Instantly share code, notes, and snippets.

@mathphreak
Last active November 2, 2022 14:40
Show Gist options
  • Save mathphreak/04e35a4a21902e5e5f8a5c283f6a8795 to your computer and use it in GitHub Desktop.
Save mathphreak/04e35a4a21902e5e5f8a5c283f6a8795 to your computer and use it in GitHub Desktop.
bitsy in twine

Embeds Bitsy games into a Twine story.

Requires Python 3. Probably only works with Twine 2 stories in the Harlowe format. Not guaranteed to work even then.

Usage

Download bitsy-in-twine.py.

λ python bitsy-in-twine.py -h
usage: bitsy-in-twine.py [-h] twine-story bitsy-game [bitsy-game ...] output

Embed Bitsy games into a Twine story.

positional arguments:
  twine-story  the exported Twine story
  bitsy-game   an exported Bitsy game
  output       where to store the result

optional arguments:
  -h, --help   show this help message and exit
  
λ python bitsy-in-twine.py twine-export.html bitsy-foo.html bitsy-bar.html out.html
#!/usr/bin/env python3
#
# Embeds Bitsy games into a Twine story.
#
# Bitsy games should be exported as .html and renamed as `bitsy-foo.html`, where `foo` can be replaced
# with any other id.
# In Twine, games can be embedded as <canvas id="bitsy-foo"></canvas>, where `foo` matches the id
# of the Bitsy game.
# WARNING: the following code sucks complete ass and was a mistake to have even written.
import argparse
from pathlib import Path
parser = argparse.ArgumentParser(description='Embed Bitsy games into a Twine story.')
parser.add_argument('twine', metavar='twine-story', type=Path, help='the exported Twine story')
parser.add_argument('bitsies', metavar='bitsy-game', type=Path, nargs='+', help='an exported Bitsy game')
parser.add_argument('output', type=Path, help='where to store the result')
args = parser.parse_args()
twine_path = args.twine
bitsy_paths = args.bitsies
output_path = args.output
# This function from the Bitsy engine needs to be replaced with this version that takes an id
# so that multiple Bitsy games can be played from the same HTML page.
# Also, this function needs to exist so things don't explode.
replacement_startExportedGame = '''
function startExportedGame(id) {
console.log('STARTING GAME', id);
attachCanvas(document.getElementById("bitsy-" + id));
load_game(document.getElementById("exportedGameData-" + id).text.slice(1));
}
'''
# This JS needs to be injected into the Twine story so the Bitsy games can be started.
twine_hook = '''
var targetNode = document.querySelector('tw-story');
var config = { childList: true };
var callback = (mutationsList) => {
for (var mutation of mutationsList) {
for (var item of mutation.addedNodes) {
if (item.className === 'transition-out') continue;
var canvas = item.querySelector('canvas[id^=bitsy-]');
if (canvas !== null) {
var id = canvas.id.replace(/^bitsy-/, '');
startExportedGame(id);
}
}
}
};
var observer = new MutationObserver(callback);
observer.observe(targetNode, config);
'''
print('Parsing Bitsy games...')
# We're going to need traversal later, so we might as well do this the right way now.
from html.parser import HTMLParser as RawHTMLParser
import re
class HTMLParser(RawHTMLParser):
def __init__(self, path, **kwargs):
super().__init__(**kwargs)
self.path = path
self.feed(path.read_text('utf-8'))
self.close()
class BitsyParser(HTMLParser):
def __init__(self, path):
filename = path.name
match_obj = re.fullmatch(r'bitsy-(.*).html', filename)
self.id = match_obj.group(1)
self.engine = ''
self.expecting_engine = False
self.game_data = ''
self.expecting_game_data = False
super().__init__(path)
print('Adding Bitsy hook to Twine...')
self.engine += twine_hook
def handle_starttag(self, tag, attrs):
if tag == 'script':
if len(attrs) == 0:
self.expecting_engine = True
elif attrs[0] == ('type', 'bitsyGameData'):
self.expecting_game_data = True
def handle_data(self, data):
if self.expecting_engine:
if data.lstrip().startswith('function startExportedGame()'):
print('Injecting support for multiple Bitsy games...')
data = replacement_startExportedGame
self.engine += data
elif self.expecting_game_data:
self.game_data += data
def handle_endtag(self, tag):
self.expecting_engine = False
self.expecting_game_data = False
class TwineParser(HTMLParser):
def __init__(self, input_path, bitsy_parsed, output_path):
self.bitsy_parsed = bitsy_parsed
self.input_path = input_path
self.output_path = output_path
self.output_file = output_path.open(mode='w', encoding='utf-8')
self.expecting_harlowe = False
super().__init__(input_path, convert_charrefs=False)
self.output_file.close()
def print(self, text):
print(text, end='', file=self.output_file)
def parse_attrs(self, tag, attrs):
if tag == 'script':
if ('data-main', 'harlowe') in attrs:
self.expecting_harlowe = True
def process_attr(attr):
k, v = attr
if v is None:
return k
else:
return k + '="' + v + '"'
attr_texts = [' ' + process_attr(attr) for attr in attrs]
return ''.join(attr_texts)
def handle_starttag(self, tag, attrs):
tag_text = '<' + tag + self.parse_attrs(tag, attrs) + '>'
self.print(tag_text)
def handle_endtag(self, tag):
if tag == 'head':
print('Adding game data to Twine file...')
for bitsy_game in self.bitsy_parsed:
id = bitsy_game.id
self.print('\n<script type="bitsyGameData" id="exportedGameData-' + id + '">')
self.print(bitsy_game.game_data)
self.print('</script>\n')
elif tag == 'body':
print('Injecting Bitsy engine to Twine...')
self.print('\n<script>')
self.print(self.bitsy_parsed[0].engine)
self.print('</script>\n')
self.print('</' + tag + '>')
def handle_startendtag(self, tag, attrs):
tag_text = '<' + tag + self.parse_attrs(tag, attrs) + ' />'
self.print(tag_text)
def handle_data(self, data):
if self.expecting_harlowe:
print('Disabling JS strict mode...')
data = data.replace('"use strict";', '', 1)
self.expecting_harlowe = False
self.print(data)
def handle_entityref(self, name):
self.print('&' + name + ';')
def handle_charref(self, name):
self.print('&#' + name + ';')
def handle_comment(self, name):
print('AAA comment')
def handle_decl(self, decl):
self.print('<!' + decl + '>')
def handle_pi(data):
print('AAAA Processing Instruction')
bitsy_parsed = [BitsyParser(x) for x in bitsy_paths]
twine_parsed = TwineParser(twine_path, bitsy_parsed, output_path)
print('All done!')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment