Compare commits

..

38 Commits

Author SHA1 Message Date
9bed89b067 Corrected server address 2022-09-03 19:50:09 +01:00
7e262e1bdb Fixed illegible fonts 2022-09-03 19:49:16 +01:00
8187648933 Corrected dark mode style conflict 2022-09-02 13:03:12 +01:00
d95f30a5e6 Example .env 2022-09-01 20:53:59 +01:00
7017188708 Compile built client 2022-09-01 20:35:39 +01:00
0ff04818ae Containerise for deployment 2022-09-01 20:35:05 +01:00
aa20416aa1 Typos 2022-09-01 11:56:27 +01:00
0c49d22865 Bugfix: adding zero counts that were skipped 2022-09-01 11:56:15 +01:00
b2df303632 Finessed a question 2022-09-01 01:37:00 +01:00
e1bce471fe Deleting redundant line 2022-08-31 16:40:15 +01:00
afbb41f7bd Remove the playbook feature text from storage 2022-08-31 14:50:47 +01:00
c6b656cbee Added all API features 2022-08-31 14:50:16 +01:00
4b0f63f124 Cleaned up some imports 2022-08-31 14:50:02 +01:00
da35b9c051 Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/wanderhome-quiz into development 2022-08-31 01:49:59 +01:00
5c30a974a6 Added database actions in view 2022-08-31 01:19:15 +01:00
713c6beb48 Updated database model 2022-08-31 01:13:49 +01:00
42534e13a9 Added tools to store lists as json 2022-08-31 01:12:44 +01:00
cadccf1a53 Updated API path 2022-08-31 00:49:00 +01:00
ce4694830d Added CORS extension 2022-08-31 00:48:36 +01:00
f7c6081ca0 Simplified question route 2022-08-31 00:48:13 +01:00
a50926b192 Typo 2022-08-29 19:41:32 +01:00
1218dbc911 Fixed a typo 2022-08-29 19:33:42 +01:00
f9d76a12ea Changed annotation 2022-08-29 19:32:19 +01:00
8a15cbef51 Wrote CLI based interface to test headless API 2022-08-29 17:11:52 +01:00
34908a2eef Added question parameter for maximum points 2022-08-29 17:11:26 +01:00
a16d9a4852 Changed question type property to select number 2022-08-29 17:10:47 +01:00
bda80b5a11 Crrected data level in json file 2022-08-29 17:09:48 +01:00
6762226bf2 Moved render question function to quiz module 2022-08-29 17:09:29 +01:00
a064bd6b9f Added submission processing 2022-08-29 17:09:07 +01:00
49b986834f Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/wanderhome-quiz into development 2022-08-24 16:03:19 +01:00
fbd17907bd delete question md file 2022-08-24 16:01:33 +01:00
f6f25d73a4 delete question md file 2022-08-24 16:01:33 +01:00
68533992b2 Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/wanderhome-quiz into development 2022-08-24 16:00:00 +01:00
c9708e761e Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/wanderhome-quiz into development 2022-08-24 16:00:00 +01:00
2d04ae5bd8 Started writing data and server 2022-08-24 15:55:53 +01:00
c1962714d1 Started writing data and server 2022-08-24 15:55:53 +01:00
e198663adb added questions 2022-08-24 14:08:05 +01:00
c62b8196dd Drafting questions 2022-08-23 15:42:26 +01:00
69 changed files with 3020 additions and 0 deletions

17
.env.example Normal file
View File

@ -0,0 +1,17 @@
SERVER_NAME= # URL where this will be hosted.
FLASK_DEBUG=False
TZ=Europe/London # Time Zone
## App Configuration
SECRET_KEY= # Long, secure, secret string.
DATABASE_TYPE=SQLite # SQLite or MySQL, defaults to SQLite
DATABASE_HOST= # Required if MySQL. Must match name of Docker service, or provide host if database is on an external server. Defaults to localhost.
DATABASE_PORT= # Required if MySQL. Defaults to 3306
## MySQL Database Configuration (Required if configured to MySQL Database.)
# Note that if using the Docker service, these configuration values will also be used when creating the database in the mysql container.
MYSQL_RANDOM_ROOT_PASSWORD=True
MYSQL_DATABASE= # Required if MySQL.
MYSQL_USER= # Required if MySQL
MYSQL_PASSWORD= # Required if MySQL. Create secure password string. Note '@' character cannot be used.

2
.gitignore vendored
View File

@ -280,3 +280,5 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# Ignore Database File
**/data/database.db

30
docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
version: '3.9'
volumes:
app:
services:
nginx:
container_name: wanderhome_nginx
image: nginx:alpine
volumes:
- ./src:/usr/share/nginx/html:ro
- ./nginx:/etc/nginx:ro
ports:
- 80:80
- 443:443
restart: unless-stopped
depends_on:
- wanderhome
wanderhome:
container_name: wanderhome_server
image: wanderhome
build: ./server
env_file:
- ./.env
ports:
- 5000:5000
volumes:
- app:/app/data
restart: unless-stopped

View File

@ -0,0 +1,6 @@
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;

View File

@ -0,0 +1,17 @@
server {
server_name _;
listen 80 default_server;
listen [::]:80 default_server;
# Proxy API requests to the server
location ^~ /api/ {
include /etc/nginx/conf.d/proxy_headers.conf;
proxy_pass http://wanderhome:5000;
}
# Proxy to the main app for all other requests
location / {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/;
}
}

25
nginx/fastcgi.conf Normal file
View File

@ -0,0 +1,25 @@
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;

24
nginx/fastcgi_params Normal file
View File

@ -0,0 +1,24 @@
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;

98
nginx/mime.types Normal file
View File

@ -0,0 +1,98 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/wasm wasm;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

30
nginx/nginx.conf Normal file
View File

@ -0,0 +1,30 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
server_tokens off;
#gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/conf.d/sites-enabled/*.conf;
}

17
nginx/scgi_params Normal file
View File

@ -0,0 +1,17 @@
scgi_param REQUEST_METHOD $request_method;
scgi_param REQUEST_URI $request_uri;
scgi_param QUERY_STRING $query_string;
scgi_param CONTENT_TYPE $content_type;
scgi_param DOCUMENT_URI $document_uri;
scgi_param DOCUMENT_ROOT $document_root;
scgi_param SCGI 1;
scgi_param SERVER_PROTOCOL $server_protocol;
scgi_param REQUEST_SCHEME $scheme;
scgi_param HTTPS $https if_not_empty;
scgi_param REMOTE_ADDR $remote_addr;
scgi_param REMOTE_PORT $remote_port;
scgi_param SERVER_PORT $server_port;
scgi_param SERVER_NAME $server_name;

16
nginx/uwsgi_params Normal file
View File

@ -0,0 +1,16 @@
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

4
server/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
env/
venv/
__pycache__/
data/database.db

143
server/CLIent.py Normal file
View File

@ -0,0 +1,143 @@
import requests
from typing import Union
SERVER = 'http://127.0.0.1:5000/'
INSTRUCTIONS = [
'---- Instructions ----',
'This quiz will present a series of multiple choice questions.',
'You can select your response by entering the index number corresponding with your chosen option(s).',
'For most questions, you may only select one option.',
'Some questions allow multiple options to be selected, and will advice you accordingly.',
'To select multiple options, enter their index number(s) separated by commas.'
]
WELCOME = [
'---- What *Wanderhome* Playbook Are You? ----',
'',
'*Wanderhome* is about going on a journey, and reflecting on how the journey changes us.',
'The road is full of myriad travellers, each with their rich lives and stories.',
'This quiz will help you figure out what kinds of stories you will bring to the table.',
'Will you join me?',
''
]
GOODBYE = [
'---- Thank you for taking the What Wanderhome Playbook Are You? Quiz ----',
'Programmed and Written by Vivek Santayana',
'Based on the table-top role-playing game *Wanderhome* by Jay Dragon, published by Possum Creek Games.',
'Check out Wanderhome at https://possumcreekgames.com/en-gb/pages/wanderhome',
'I strongly recommend this game: it is amazing!',
'',
'Good bye, and may your journeys surprise you!',
''
]
def sanitise_text(raw_text:str) -> str:
format_replacements = {
'<p>': '',
'</p>': '\n',
'<em>': '*',
'</em>': '*',
'<strong>': '**',
'</strong>': '**',
'&#xe6;': 'æ',
'&lsquo;': '',
'&rsquo;': '',
'&iuml;': 'ï'
}
for key, value in format_replacements.items():
raw_text = raw_text.replace(key, value)
return raw_text
def render(output:list):
print('\n'.join(['', *output, '']))
x = input('Press Enter to proceed.')
def render_question(question) -> str:
print(sanitise_text(raw_text=question['question']))
print('-- Options --')
for index, answer in enumerate(question['answers']):
print(index, sanitise_text(raw_text=answer))
print('')
prompt = f'Please select up to {question["select"]} options, separated by commas: ' if question['select'] > 1 else 'Please select an answer: '
return input(prompt)
def parse_input(raw_answer) -> list:
try: return list(set(int(answer) for answer in raw_answer.split(',')))
except: return [ ]
def process_input(answer_input:list, question:dict) -> Union[list, int]: return answer_input[0] if question['select'] == 1 else answer_input
def show_input_error(): input('\n *** Your response was invalid. Please try again *** \n\nPress enter to retry.')
def validate_answer(answers:list, question:dict) -> bool:
def check_range(answer:int) -> bool: return answer >= 0 and answer < len(question['answers'])
def check_input_number(answers:list) -> bool: return len(answers) > 0 and len(answers) <= question['select']
if not check_input_number(answers=answers): return False
try: return all([check_range(answer=int(answer)) for answer in answers])
except ValueError: return False
def render_questions() -> dict:
answers = [ ]
try:
questions = requests.get(url=f'{SERVER}/api/questions/').json()
except Exception as exception:
print(exception)
quit()
for index, question in enumerate(questions):
answer_valid = False
while not answer_valid:
print('')
print(f'---- Question {index + 1} ----')
answer = render_question(question)
answer = parse_input(answer)
answer_valid = validate_answer(answer, question)
if not answer_valid: show_input_error()
answers.append(process_input(answer, question))
try: return requests.post(url=f'{SERVER}api/submit/',json=answers).json()
except Exception as exception:
print(exception)
quit()
def render_results(results:dict):
print('\n---- Results ----\n')
plural = len(results['playbooks']) > 1
print(f'Your { "results are" if plural else "result is"}:')
for playbook in results['playbooks']:
name = list(playbook.keys())[0]
data = list(playbook.values())[0]
animals = ''
for index, animal in enumerate(data['animals']):
article = 'an' if animal[0] in ['a','e','i', 'o', 'u'] else 'a'
conjunction = ', or ' if index == len(data['animals']) - 2 else '.' if index == len(data["animals"]) - 1 else ', '
animals += f'{article} {animal}{conjunction}'
print(f'\n**The {name[0].upper()}{name[1:]}**\n(pp.{data["pages"]})\n')
print(sanitise_text(data['flavour']),'\n')
print(sanitise_text(data['blurb']),'\n')
print('You are alive. Your care is ',sanitise_text(data['care']),'.\n')
print('Your animal form is ', animals, '\n')
print('You can always:')
for action in data['actions']:
print('-- ', sanitise_text(action))
input('\nPress enter to continue.')
print('\n-- Your score for each playbook: --')
for playbook, score in results['all_playbooks'].items(): print(f'The {playbook[0].upper()}{playbook[1:]}: {score*"x"} ({round(100*score/results["max_score"])}%)')
def run_quiz():
render(output=WELCOME)
render(output=INSTRUCTIONS)
run = True
while run:
render_results(render_questions())
repeat_prompt_valid = False
while not repeat_prompt_valid:
repeat_prompt = input('\nDo you want to do the quiz again? (y) Yes / (n) No: ')
if repeat_prompt.lower() in ['y', 'n', 'yes', 'no']: repeat_prompt_valid = True
if not repeat_prompt_valid: show_input_error()
run = True if repeat_prompt.lower() in ['y', 'yes'] else False
render(GOODBYE)
"""Running the Quiz"""
if __name__ == '__main__': run_quiz()

8
server/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM python:3.10-slim
ARG DATA=./data/
ENV DATA=$DATA
WORKDIR /app
COPY . .
RUN pip install --upgrade pip && pip install -r requirements.txt
RUN chmod +x install.py && ./install.py
CMD [ "gunicorn", "-b", "0.0.0.0:5000", "-w", "5", "wsgi:app" ]

28
server/app/__init__.py Normal file
View File

@ -0,0 +1,28 @@
from .config import Development as Config
from .models import *
from .extensions import cors, db
from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix
def create_app():
app = Flask(__name__)
app.config.from_object(Config())
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto = 1, x_host = 1)
cors.init_app(app=app)
db.init_app(app=app)
from .views import views
app.register_blueprint(
blueprint = views,
url_prefix = '/api'
)
"""Create database before first request"""
@app.before_first_request
def _create_database_tables():
with app.app_context(): db.create_all()
return app

38
server/app/config.py Normal file
View File

@ -0,0 +1,38 @@
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv('../.env')
class Config(object):
"""Basic App Configuration"""
APP_HOST = '0.0.0.0'
DATA = './data/'
DEBUG = False
TESTING = False
SECRET_KEY = os.getenv('SECRET_KEY')
SERVER_NAME = os.getenv('SERVER_NAME')
SESSION_COOKIE_SECURE = True
"""Database Driver Configuration"""
DATABASE_TYPE = os.getenv('DATABASE_TYPE') or 'SQLite'
SQLALCHEMY_TRACK_MODIFICATIONS = False
if DATABASE_TYPE.lower() == 'mysql' and os.getenv('MYSQL_DATABASE') and os.getenv('MYSQL_USER') and os.getenv('MYSQL_PASSWORD'):
DATABASE_HOST = os.getenv('DATABASE_HOST') or 'localhost'
DATABASE_PORT = int(os.getenv('DATABASE_PORT') or 3306)
MYSQL_DATABASE = os.getenv('MYSQL_DATABASE')
MYSQL_USER = os.getenv('MYSQL_USER')
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD')
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{MYSQL_DATABASE}'
else: SQLALCHEMY_DATABASE_URI = f'sqlite:///{Path(os.path.abspath(f"{DATA}/database.db"))}'
class Production(Config): pass
class Development(Config):
APP_HOST = '127.0.0.1'
DEBUG = True
SERVER_NAME = '127.0.0.1:5000'
SESSION_COOKIE_SECURE = False
class Testing(Development):
TESTING = True

4
server/app/extensions.py Normal file
View File

@ -0,0 +1,4 @@
from flask_cors import CORS
cors = CORS()
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

View File

@ -0,0 +1 @@
from .entry import Entry

View File

@ -0,0 +1,27 @@
from ..extensions import db
from ..tools.data import declutter_results
from ..tools.models import JsonString
from sqlalchemy_json import MutableJson
from datetime import datetime
from uuid import uuid4
class Entry(db.Model):
id = db.Column(db.String(36), primary_key=True)
timestamp = db.Column(db.DateTime, nullable=False)
answers = db.Column(JsonString, nullable=False)
results = db.Column(MutableJson, nullable=False)
def __repr__(self) -> str: return f'Entry with <id {self.id}>.'
def __init__(self, answers:list, results:dict):
self.id = uuid4().hex
self.timestamp = datetime.utcnow()
self.answers = answers.copy()
self.results = declutter_results(results)
def add(self):
db.session.add(self)
db.session.commit()

View File

24
server/app/tools/data.py Normal file
View File

@ -0,0 +1,24 @@
from flask import current_app as app
import json
from os.path import isfile
from pathlib import Path
from copy import deepcopy
def check_file(filename:str) -> bool:
data_dir = Path(app.config.get('DATA'))
if isfile(f'./{data_dir}/{filename}'): return True
return False
def load(filename:str) -> dict:
data_dir = Path(app.config.get('DATA'))
with open(f'./{data_dir}/{filename}') as file: return json.load(file)
def declutter_results(raw_results:dict) -> dict:
data = deepcopy(raw_results)
playbook_lists = [ ]
for item in data['playbooks']:
playbook_lists.append(list(item.keys())[0])
data['playbooks'] = playbook_lists
return data

View File

@ -0,0 +1,16 @@
from sqlalchemy import JSON, TypeDecorator
from sqlalchemy.dialects.sqlite import JSON
from json import dumps, loads
class JsonString(TypeDecorator):
"""Enables JSON storage by encoding and decoding on the fly."""
impl = JSON
def process_bind_param(self, value, dialect):
return dumps(value)
def process_result_value(self, value, dialect):
return loads(value)

37
server/app/tools/quiz.py Normal file
View File

@ -0,0 +1,37 @@
from .data import load
def render_questions() -> list:
data = load('questions.json')
for question in data:
_answers = [ answer['text'] for answer in question['answers'] ]
question['answers'] = _answers
question.pop('max', None)
return data
def evaluate_answers(answers:list) -> dict:
playbooks = load('playbooks.json')
questions = load('questions.json')
scores = dict.fromkeys(playbooks,0)
for index, answer in enumerate(answers):
question = questions[index]
if type(answer) is list:
answer = answer[0:question['select']]
for _answer in answer:
for match in question['answers'][int(_answer)]['matches']: scores[match] += 1
else:
for match in question['answers'][int(answer)]['matches']: scores[match] += 1
return scores
def compile_results(results:dict) -> dict:
output = {
'all_playbooks': results.copy(),
'playbooks': [ ],
'score': max(results.values()),
'max_score': 0
}
for question in load('questions.json'):
output['max_score'] += question.pop('max', 1)
playbooks = load('playbooks.json')
for playbook, score in results.items():
if score == output['score']: output['playbooks'].append({playbook: playbooks[playbook].copy()})
return output

65
server/app/views.py Normal file
View File

@ -0,0 +1,65 @@
from .models import Entry
from .tools.data import load
from .tools.quiz import compile_results, evaluate_answers, render_questions
from flask import Blueprint, jsonify, request
import numpy
views = Blueprint(
name='views',
import_name=__name__
)
@views.route('/questions/')
def _questions():
return render_questions()
@views.route('/submit/', methods=['POST'])
def _submit():
answers = request.json
scores = evaluate_answers(answers)
results = compile_results(results=scores)
new_entry = Entry(answers=answers, results=results)
new_entry.add()
return jsonify(results)
@views.route('/count/')
def _count():
return jsonify(len(Entry.query.all()))
@views.route('/playbooks/')
def _playbooks():
playbooks = dict.fromkeys(load('playbooks.json'), 0)
for entry in Entry.query.all():
for _playbook in entry.results['playbooks']: playbooks[_playbook] += 1
return playbooks
@views.route('/answers/')
def _answers():
answers = { }
for index, question in enumerate(render_questions()):
answers[index] = { }
for _index, answer in enumerate(question['answers']): answers[index][_index] = 0
for entry in Entry.query.all():
for index, answer in enumerate(entry.answers):
if type(answer) is list:
for option in answer: answers[index][option] += 1
else:
answers[index][answer] += 1
return list(answers.values())
@views.route('/scores/')
def _scores():
playbooks = { playbook: list() for playbook in load('playbooks.json') }
for playbook, scores in playbooks.items():
for entry in Entry.query.all():
score = entry.results['all_playbooks'][playbook]
percentage_score = 100 * score/entry.results['max_score']
scores.append(percentage_score)
output = { playbook: dict() for playbook in playbooks }
for playbook, scores in playbooks.items():
output[playbook]['mean'] = numpy.mean(scores)
output[playbook]['median'] = numpy.median(scores)
output[playbook]['standard_deviation'] = numpy.std(scores)
return output

333
server/data/playbooks.json Normal file
View File

@ -0,0 +1,333 @@
{
"caretaker": {
"blurb": "The Caretaker pays attention and tends to the small and forgotten gods, helping each one find a home.",
"pages": "48-51",
"flavour": "Someone must pay attention to all the small and forgotten things in the world. Someone must listen to the voiceless.",
"care": "tender, supportive, and silent",
"animals": [
"stoat",
"salamander",
"lemur",
"crow",
"reflective animal",
"quiet animal"
],
"actions": [
"Pause, tilt your head to the side, and keep going.",
"Play with one of your gods.",
"Say something in silence better than words can.",
"Notice a little friend everyone else overlooked.",
"Say: &lsquo;<em>Hold this.</em>&rsquo;",
"Ask: &lsquo;<em>Hush, can you hear that?</em>&rsquo;"
]
},
"dancer": {
"blurb": "The Dancer opens up to all the magical and strange forces in the world, inviting them to dance and exist in the moment.",
"pages": "52-55",
"flavour": "Your inner fire is a song in your heart, not a voice in your head. The world will not quiet you.",
"care": "intense, vocal, and full of light",
"animals": [
"fox",
"heron",
"tamarin",
"skink",
"lively animal",
"hopeful animal"
],
"actions": [
"Dance idly.",
"Laugh and smile.",
"Leap up on top of something.",
"Give a song to those around you.",
"Ask: &lsquo;Will you dance a dance with me?&rsquo;",
"Ask: &lsquo;Do you want to be my friend?&rsquo;"
]
},
"exile": {
"blurb": "Banished from their homeland, the Exile travels through the H&#xe6;th looking for somewhere that can take them in and help them heal.",
"pages": "56-59",
"flavour": "Your past clings to your shoulders like an old woolen cloak. Someday, perhaps, you can return to your home.",
"care": "fragile, skittish, and terrified of being broken again",
"animals": [
"deer",
"tiger",
"eagle",
"skunk",
"rare animal",
"nomadic animal"
],
"actions": [
"Say an expression in your traditional language.",
"Keep an eye on the exits.",
"Push something out of sight or out of mind.",
"Play a tune that reminds you of home.",
"Say: &lsquo;You look familiar.&rsquo;",
"Ask: &lsquo;Can I tell you a story about my home?&rsquo;"
]
},
"firelight": {
"blurb": "The Firelight is accompanied by a firefly that lights their path and helps guide people through the world.",
"pages": "60-63",
"flavour": "It is easy to get lost in the darkness and the deep. The firefly at your side will always guide the way.",
"care": "forward-thinking, mutual, and shining bright",
"animals": [
"racoon",
"olm",
"albatross",
"sphynx cat",
"nocturnal animal",
"steady animal"
],
"actions": [
"Shrug.",
"Pet your firefly.",
"Illuminate all that is hard to see.",
"Keep walking.",
"Say: &lsquo;Watch your step.&rsquo;",
"Ask: &lsquo;Do you need a hand?&rsquo;"
]
},
"fool": {
"blurb": "The Fool is unused to and unfamiliar with the complicated and tangled world outside, and approaches everything with the same naїve optimism.",
"pages": "64-67",
"flavour": "Life is an adventure, and there&rsquo;s never shame in learning new things.",
"care": "silly, na&iuml;ve, and more profound than most expect",
"animals": [
"parrot",
"star-nosed mole",
"samoyed",
"anole",
"goofy animal",
"oblivious animal"
],
"actions": [
"Meander around.",
"Give great advice.",
"Give terrible advice.",
"Do something that&rsquo;s actually pretty funny.",
"Ask: &lsquo;But why?&rsquo;",
"Ask: &lsquo;What&rsquo;s going on?&rsquo;"
]
},
"guardian": {
"blurb": "The Guardian takes care of a ward, a young child with a difficult past and in desperate need of care.",
"pages": "68-71",
"flavour": "Hold your ward close to your heart. Someday the world will hurt them, but this will not be that day.",
"care": "paternal, protective, and unconditional",
"animals": [
"bear",
"hen",
"wolf",
"rabbit",
"wary animal",
"strong animal"
],
"actions": [
"Sigh and shake your head.",
"Keep both eyes on someone.",
"Derive a practical lesson from a situation.",
"Tell a story that makes someone embarrassed.",
"Ask: &lsquo;Where do you think you&rsquo;re going?&rsquo;",
"Ask: &lsquo;Where is my ward?&rsquo;"
]
},
"moth-tender": {
"blurb": "The Moth-Tender assists the carrier moths that fly across the H&#xe6;th, delivering letters and parcels to everyone who needs them.",
"pages": "72-75",
"flavour": "Carrier moths travel across the H&#xe6;th, bringing news, letters, and tiny boxes. You wander the land, keeping an eye on these moths and their towers.",
"care": "consistent and prompt, and it arrives in small packages",
"animals": [
"bat",
"horse",
"pigeon",
"rabbit",
"persistent animal",
"dogged animal"
],
"actions": [
"Follow the moths.",
"Fidget.",
"Write something down on a piece of paper.",
"Tell everyone what phase the moon is in right now.",
"Ask: &lsquo;Have you heard the news?&rsquo;",
"Say: &lsquo;I have a letter for you!&rsquo;"
]
},
"peddler": {
"blurb": "The Peddler transports supplies and wares from place to place, ensuring that each community has access to everything they need to survive.",
"pages": "76-79",
"flavour": "The road is long and filled with merchants, traders, and everyone else doing their part to make sure everywhere in the H&#xe6;th is provided for. No one&rsquo;s home can stand alone.",
"care": "material, solid, and dependable",
"animals": [
"donkey",
"condor",
"llama",
"crocodile",
"rugged animal",
"tireless animal"
],
"actions": [
"Know someone who can help.",
"Intensely examine something.",
"Sit down and do the math.",
"Trade for or trade away one of your many wares.",
"Ask: &lsquo;What can I do for you?&rsquo;",
"Say: &lsquo;I have a deal for you.&rsquo;"
]
},
"pilgrim": {
"blurb": "In search of a faraway place, the Pilgrim is carried by their faith and desire to reach a home that might not even exist.",
"pages": "80-83",
"flavour": "The gods have given you a path forward, a place you hope can finally give you what you seek. Some days you worry you&rsquo;ll never make it there at all.",
"care": "enduring, faithful, and expressed one step at a time",
"animals": [
"ibis",
"bison",
"ferret",
"newt",
"devoted animal",
"ceaseless animal"
],
"actions": [
"Recite a small prayer.",
"Chatter away.",
"Shield yourself from harsh conditions.",
"Place your fate in improbable coincidence, and have it work out.",
"Ask: &lsquo;Do you think we&rsquo;ll make it?&rsquo;",
"Say: &lsquo;Lead the way.&rsquo;"
]
},
"poet": {
"blurb": "The Poet is a writer using their journeys as inspiration for their project, trying to tie together the threads that intertwine the history of this land with their own heart.",
"pages": "84-87",
"flavour": "The song of the world is a poem that can be captured by ink and paper, if only you could find the right words.",
"care": "eloquent, observant, and occasionally overwrought",
"animals": [
"porcupine",
"rook",
"terrier",
"toad",
"lyrical animal",
"pensive animal"
],
"actions": [
"Self-deprecate.",
"Cite your sources, in hopes that they can help.",
"Write down a moment that feels relevant to your project.",
"Provide a new perspective others might not have.",
"Ask: &lsquo;What used to be here?&rsquo;",
"Ask: &lsquo;Can you explain?&rsquo;"
]
},
"ragamuffin": {
"blurb": "The Ragamuffin is a little rascal of a kid who just wants to cause problems and have fun.",
"pages": "88-91",
"flavour": "Run! Scream! Play! Steal! And above all, live!",
"care": "young, exuberant, and na&iuml;ve",
"animals": [
"otter",
"gecko",
"capuchin",
"kitten",
"cute animal",
"young animal"
],
"actions": [
"Get distracted.",
"Get really invested in a new interest.",
"Blurt out a secret.",
"Somehow manage to squeeze yourself out of trouble.",
"Ask: &lsquo;Do you wanna hang out with me?&rsquo;.",
"Ask: &lsquo;Do you wanna see something really cool?&rsquo;"
]
},
"shepherd": {
"blurb": "The Shepherd tends to a herd of bumblebees, keeping an eye out with them as they travel to different pastures.",
"pages": "92-95",
"flavour": "Herds of chubby bumblebees can be found across the Hæth, and tending to those flocks is simple, honest work.",
"care": "measureless, watchful, and gentle",
"animals": [
"ram",
"turtle",
"sheepdog",
"hawk",
"guiding animal",
"peaceful animal"
],
"actions": [
"Pat a bumble on its head.",
"Stare off into the distance.",
"Make an offhand observation that turns out to be correct.",
"Rest your back against something and take a moment to breathe.",
"Say: &lsquo;They&rsquo;re friendly, don&rsquo;t fret.&rsquo;",
"Ask: &lsquo;Can I teach you something someone once taught me?&rsquo;"
]
},
"teacher": {
"blurb": "The Teacher is a traveling professor, who visits kids throughout the H&#xe6;th to instruct them on specialized knowledge and hidden secrets.",
"pages": "96-99",
"flavour": "Across the H&#xe6;th, there are students who need teaching. You travel from town to town and help them learn the little pieces they might not yet know.",
"care": "wise, distant, and gentle",
"animals": [
"owl",
"hare",
"chameleon",
"orangutan",
"wise animal",
"attentive animal"
],
"actions": [
"Sit down, surrounded by others.",
"Fumble for your supplies.",
"Clear your throat and get everyone&rsquo;s attention.",
"Tell the table about something related to a subject you teach.",
"Ask: &lsquo;Can you show me?&rsquo;",
"Ask: &lsquo;What can we learn from this?&rsquo;"
]
},
"vagabond": {
"blurb": "The Vagabond was once convicted by a faraway and cruel authority, and has been forced to reinvent themself on the road.",
"pages": "100-103",
"flavour": "The world&rsquo;s taken everything from you, beat down on your shoulders, and given you an aching heart. Some people think you&rsquo;re a criminal, or a monster. You know what you are.",
"care": "invisible, cautious, and unimaginably deep",
"animals": [
"possum",
"rat",
"rattlesnake",
"raven",
"misunderstood animal",
"sneaky animal"
],
"actions": [
"Be somewhere you&rsquo;re not supposed to be.",
"Have something you&rsquo;re not supposed to have.",
"Mutter something you&rsquo;re not supposed to say.",
"Lie.",
"Say: &lsquo;I have a bad feeling about this.&rsquo;",
"Ask: &lsquo;Do you trust me?&rsquo;"
]
},
"veteran": {
"blurb": "The Veteran was once a great hero, who held the entire world on the tip of their blade. No longer.",
"pages": "104-107",
"flavour": "You wield the blade that must never be drawn again.",
"care": "intense, sober, and mindful",
"animals": [
"badger",
"hellbender",
"raven",
"mouse",
"dangerous animal",
"headstrong animal"
],
"actions": [
"Repeat a calming phrase.",
"Spend time practicing a craft you&rsquo;re not very good at.",
"Drum against the pommel of your blade.",
"Leap to your feet.",
"Say: &lsquo;I don&rsquo;t do that anymore.&rsquo;",
"Ask: &lsquo;What are you hiding?&rsquo;",
"Unsheathe your blade and immediately kill the person in front of you. Then, remove your character from the game. You cannot play them any longer."
]
}
}

764
server/data/questions.json Normal file
View File

@ -0,0 +1,764 @@
[
{
"question": "<p><em>Wanderhome</em> is about a group of animals travelling through a vast, pastoral landscape.</p><p>Choose <strong>up to three</strong> personality traits to describe what kind of animal you are.</p>",
"select": 3,
"max": 2,
"answers": [
{
"text": "Dangerous",
"matches": [
"guardian",
"vagabond",
"veteran"
]
},
{
"text": "Headstrong",
"matches": [
"moth-tender",
"pilgrim",
"veteran"
]
},
{
"text": "Hopeful",
"matches": [
"dancer",
"fool",
"pilgrim"
]
},
{
"text": "Lively",
"matches": [
"dancer",
"poet",
"ragamuffin"
]
},
{
"text": "Misunderstood",
"matches": [
"exile",
"fool",
"vagabond"
]
},
{
"text": "Pensive",
"matches": [
"caretaker",
"poet",
"teacher"
]
},
{
"text": "Persistent",
"matches": [
"exile",
"moth-tender",
"peddler"
]
},
{
"text": "Quiet",
"matches": [
"caretaker",
"firelight",
"shepherd"
]
},
{
"text": "Sagacious",
"matches": [
"guardian",
"shepherd",
"teacher"
]
},
{
"text": "Tireless",
"matches": [
"firelight",
"peddler",
"ragamuffin"
]
}
]
},
{
"question": "<p>Journeying through the <em>H&#xe6;th</em> requires all travellers to care about the people and places around them.</p><p>Choose <strong>up to three</strong> characteristics that describe what your care is like.</p>",
"select": 3,
"max": 3,
"answers": [
{
"text": "Cautious",
"matches": [
"exile",
"guardian",
"shepherd",
"vagabond",
"veteran"
]
},
{
"text": "Dependable",
"matches": [
"caretaker",
"guardian",
"moth-tender",
"peddler",
"pilgrim"
]
},
{
"text": "Enduring",
"matches": [
"dancer",
"firelight",
"moth-tender",
"peddler",
"pilgrim"
]
},
{
"text": "Exuberant",
"matches": [
"dancer",
"firelight",
"fool",
"poet",
"ragamuffin"
]
},
{
"text": "Gentle",
"matches": [
"caretaker",
"exile",
"shepherd",
"teacher",
"veteran"
]
},
{
"text": "Intense",
"matches": [
"dancer",
"firelight",
"poet",
"shepherd",
"veteran"
]
},
{
"text": "Na&iuml;ve",
"matches": [
"fool",
"guardian",
"pilgrim",
"ragamuffin",
"vagabond"
]
},
{
"text": "Subtle",
"matches": [
"caretaker",
"exile",
"moth-tender",
"teacher",
"vagabond"
]
},
{
"text": "Wise",
"matches": [
"firelight",
"fool",
"peddler",
"poet",
"teacher"
]
}
]
},
{
"question": "<p>The landscape across the H&#xe6;th has many vibrant places.</p><p>What kind of place do you come from?</p>",
"select": 1,
"answers": [
{
"text": "Comfortable places that provide shelter and food, like farms, gardens, or markets",
"matches": [
"caretaker",
"guardian",
"teacher"
]
},
{
"text": "Verdant places that are lush and inviting, like fields, glens, or lagoons",
"matches": [
"exile",
"ragamuffin",
"shepherd"
]
},
{
"text": "Liminal places that are the way between other places, like bridges, ports, or taverns",
"matches": [
"firelight",
"moth-tender",
"vagabond"
]
},
{
"text": "Sprawling places that inspire wonder, like carnivals, castles, or metropolises",
"matches": [
"dancer",
"fool",
"peddler"
]
},
{
"text": "Lonely places where the rest of the world does not intrude, like caves, graveyards, or moors",
"matches": [
"pilgrim",
"poet",
"veteran"
]
}
]
},
{
"question": "<p>Travellers in the H&#xe6;th find themselves journeying together even if their reasons for travelling are vastly different.</p><p>What was the reason that made you embark on your journey?</p>",
"select": 1,
"answers": [
{
"text": "A past that I want to leave behind",
"matches": [
"exile",
"fool",
"veteran"
]
},
{
"text": "A future I want to go towards",
"matches": [
"dancer",
"guardian",
"pilgrim"
]
},
{
"text": "A desire to learn more and see the world",
"matches": [
"firelight",
"poet",
"teacher"
]
},
{
"text": "A want for the riches and bounties of the H&#xe6;th",
"matches": [
"peddler",
"moth-tender",
"vagabond"
]
},
{
"text": "A need to be with the people on the trail",
"matches": [
"caretaker",
"ragamuffin",
"shepherd"
]
}
]
},
{
"question": "<p>Along our travels, we must be prepared for whatever the road has in store for us.</p><p>What thing do you carry spares of should your companions not have it already?</p>",
"select": 1,
"answers": [
{
"text": "Blankets for when it is chilly",
"matches": [
"exile",
"moth-tender",
"shepherd"
]
},
{
"text": "Torches to light dark tunnels",
"matches": [
"firelight",
"peddler",
"vagabond"
]
},
{
"text": "Snacks for when we are peckish",
"matches": [
"guardian",
"teacher",
"ragamuffin"
]
},
{
"text": "A song or story for when it is too quiet",
"matches": [
"dancer",
"fool",
"poet"
]
},
{
"text": "Incense for when we need to show respect",
"matches": [
"caretaker",
"pilgrim",
"veteran"
]
}
]
},
{
"question": "<p>Our journey will take us through the sweeping arc of the seasons.</p><p>Which season do you look forward to the most?</p>",
"select": 1,
"answers": [
{
"text": "Leap: a time of melting frost and great rain",
"matches": [
"caretaker",
"exile",
"vagabond"
]
},
{
"text": "Bright: when flowers bloom and the sun blazes",
"matches": [
"dancer",
"pilgrim",
"ragamuffin"
]
},
{
"text": "Breathe: a season of swarming bugs and fleeting days",
"matches": [
"moth-tender",
"peddler",
"shepherd"
]
},
{
"text": "Silt: when the leaves turn red and fall as the days get colder",
"matches": [
"fool",
"poet",
"teacher"
]
},
{
"text": "Chill: a season hushed in deep snow and thick frost",
"matches": [
"firelight",
"guardian",
"veteran"
]
}
]
},
{
"question": "<p>When travelling with companions, it may be difficult to find time alone.</p><p>What do you do to get those precious moments of solitude along the way?</p>",
"select": 1,
"answers": [
{
"text": "Wake up early before everyone else",
"matches": [
"caretaker",
"guardian",
"shepherd"
]
},
{
"text": "Stay up late after everyone else has gone to bed",
"matches": [
"firelight",
"moth-tender",
"teacher"
]
},
{
"text": "Excuse myself and retire to some place quiet",
"matches": [
"exile",
"fool",
"peddler"
]
},
{
"text": "Lose myself for a moment in vigorous activity",
"matches": [
"dancer",
"ragamuffin",
"veteran"
]
},
{
"text": "Withdraw silently into my own mind, even around other people",
"matches": [
"pilgrim",
"poet",
"vagabond"
]
}
]
},
{
"question": "<p>The H&#xe6;th is full of small gods, many forgotten, scattered amongst its many settlements and sprawling wilderness.</p><p>Which of these gods are you least likely to forget?</p>",
"select": 1,
"answers": [
{
"text": "The god of a warm, comforting bath who smells like citrus",
"matches": [
"poet",
"shepherd",
"teacher"
]
},
{
"text": "The god of overflowing tankards who will obligingly buy you a round if you ask nicely",
"matches": [
"fool",
"peddler",
"vagabond"
]
},
{
"text": "The god of dense forest canopies who will shelter you from inclement weather",
"matches": [
"exile",
"caretaker",
"veteran"
]
},
{
"text": "The god of bioluminescent mushrooms who will light your way in the dark",
"matches": [
"dancer",
"firelight",
"moth-tender"
]
},
{
"text": "The god of childish wonder who changes form for each new phenomenon you encounter",
"matches": [
"pilgrim",
"guardian",
"ragamuffin"
]
}
]
},
{
"question": "<p>Everyone in the H&#xe6;th is be fundamentally good, except the mighty whose souls may be weighed down by power or poisoned by the struggle.</p><p>Of all these people whose goodness may be in conflict, for whom do you have the greatest understanding?</p>",
"select": 1,
"answers": [
{
"text": "The monarch who ushered in great change",
"matches": [
"dancer",
"moth-tender",
"ragamuffin"
]
},
{
"text": "The lord who tried to make the land prosper",
"matches": [
"caretaker",
"peddler",
"shepherd"
]
},
{
"text": "The general who protected his people",
"matches": [
"exile",
"guardian",
"teacher"
]
},
{
"text": "The hero who sought power to defend his kin",
"matches": [
"fool",
"poet",
"vagabond"
]
},
{
"text": "The soldier who did his duty",
"matches": [
"firelight",
"pilgrim",
"veteran"
]
}
]
},
{
"question": "<p>There was once a great war that tore the H&#xe6;th asunder.</p><p>But where were you during it?</p>",
"select": 1,
"answers": [
{
"text": "In the midst of it, taking a stand for what I believe in",
"matches": [
"exile",
"moth-tender",
"peddler",
"vagabond",
"veteran"
]
},
{
"text": "On the sidelines, looking after the lost and wounded",
"matches": [
"caretaker",
"firelight",
"guardian",
"poet",
"teacher"
]
},
{
"text": "Far away, holding on to the peace",
"matches": [
"dancer",
"fool",
"pilgrim",
"ragamuffin",
"shepherd"
]
}
]
},
{
"question": "<p>The war has left its many scars along the H&#xe6;th and its people.</p><p>What shadow of the war are you trying to escape?</p>",
"select": 1,
"answers": [
{
"text": "Guilt for my part in it",
"matches": [
"exile",
"guardian",
"moth-tender",
"peddler",
"veteran"
]
},
{
"text": "Shame for not taking a stand",
"matches": [
"dancer",
"firelight",
"fool",
"pilgrim",
"vagabond"
]
},
{
"text": "Pain from its many losses",
"matches": [
"caretaker",
"poet",
"ragamuffin",
"shepherd",
"teacher"
]
}
]
},
{
"question": "<p>The time for violence has long ended, and the H&#xe6;th is now a place of peace.</p><p>Why do you believe violence is wrong?</p>",
"select": 1,
"answers": [
{
"text": "Because of who we become when we resort to violence",
"matches": [
"guardian",
"firelight",
"veteran"
]
},
{
"text": "Because of the pain it causes to those around us",
"matches": [
"exile",
"moth-tender",
"ragamuffin"
]
},
{
"text": "Because all gods, big and small, have warned us against it",
"matches": [
"caretaker",
"dancer",
"pilgrim"
]
},
{
"text": "Because peaceful solutions to conflict are just more effective",
"matches": [
"peddler",
"poet",
"shepherd"
]
},
{
"text": "Because the ends do not justify the means",
"matches": [
"fool",
"teacher",
"vagabond"
]
}
]
},
{
"question": "<p>Journeys through the Heath involve incidental companions: people who are coincidentally going the same way for now, who may part ways or end their travels should their paths diverge from their companions&rsquo;.</p><p>What would mark the end of your journey?</p>",
"select": 1,
"answers": [
{
"text": "Arriving at the place I am seeking",
"matches": [
"firelight",
"pilgrim",
"vagabond"
]
},
{
"text": "Finding the person I am looking for",
"matches": [
"guardian",
"ragamuffin",
"shepherd"
]
},
{
"text": "Completing a great task I set myself",
"matches": [
"fool",
"poet",
"teacher"
]
},
{
"text": "Becoming the person I want to be",
"matches": [
"dancer",
"exile",
"veteran"
]
},
{
"text": "Acquiring what I need",
"matches": [
"caretaker",
"moth-tender",
"peddler"
]
}
]
},
{
"question": "<p>When we first set out on our journey, we did so in search of some place to call home.</p><p>What would it take for you to call a place your home?</p>",
"select": 1,
"answers": [
{
"text": "Familiar comforts of the place I come from",
"matches": [
"teacher",
"vagabond",
"veteran"
]
},
{
"text": "Shrines and knicknacks reminding me of my time on the road",
"matches": [
"caretaker",
"firelight",
"pilgrim"
]
},
{
"text": "A picturesque landscape with majestic sunsets",
"matches": [
"fool",
"poet",
"shepherd"
]
},
{
"text": "A charming workshop where I can keep myself busy",
"matches": [
"dancer",
"moth-tender",
"peddler"
]
},
{
"text": "People with whom I belong",
"matches": [
"exile",
"guardian",
"ragamuffin"
]
}
]
},
{
"question": "<p>Journeys through the H&#xe6;th have a way of continuing even after they end.</p><p>After your travels have ended, how will you keep the journey alive?</p>",
"select": 1,
"answers": [
{
"text": "In the stories I tell or the songs I sing",
"matches": [
"dancer",
"poet",
"teacher"
]
},
{
"text": "Through the trinkets with which I will furnish my home",
"matches": [
"caretaker",
"moth-tender",
"peddler"
]
},
{
"text": "By always leaving my door open for my companions",
"matches": [
"exile",
"fool",
"shepherd"
]
},
{
"text": "By visiting old friends regularly",
"matches": [
"guardian",
"ragamuffin",
"veteran"
]
},
{
"text": "By embarking on another journey once again",
"matches": [
"firelight",
"pilgrim",
"vagabond"
]
}
]
}
]

0
server/install.py Normal file
View File

3
server/main.py Normal file
View File

@ -0,0 +1,3 @@
from app import create_app
app = create_app()
if __name__ == '__main__': app.run()

18
server/requirements.txt Normal file
View File

@ -0,0 +1,18 @@
click==8.1.3
Flask==2.2.2
Flask-Cors==3.0.10
Flask-SQLAlchemy==2.5.1
greenlet==1.1.3
gunicorn==20.1.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
numpy==1.23.2
pip==22.2.2
python-dotenv==0.20.0
setuptools==65.3.0
six==1.16.0
SQLAlchemy==1.4.40
sqlalchemy-json==0.5.0
Werkzeug==2.2.2
wheel==0.37.1

2
server/wsgi.py Normal file
View File

@ -0,0 +1,2 @@
from main import app
if __name__ == '__main__': app.run()

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

12
src/browserconfig.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="img/icons/mstile-70x70.png"/>
<square150x150logo src="img/icons/mstile-150x150.png"/>
<square310x310logo src="img/icons/mstile-310x310.png"/>
<wide310x150logo src="img/icons/mstile-310x150.png"/>
<TileColor>#1eff1e</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/img/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 78 KiB

22
src/index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="apple-touch-icon" sizes="180x180" href="img/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#1e441e">
<meta name="msapplication-TileImage" content="img/icons/mstile-144x144.png">
<meta name="theme-color" content="#1e441e">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wanderhome Quiz | V.S.</title>
<script type="module" crossorigin src="/assets/index.0b8786b7.js"></script>
<link rel="stylesheet" href="/assets/index.9745abf5.css">
</head>
<body class="bg-lime-100 h-screen">
<div class="bg-lime-100" id="app"></div>
</body>
</html>

19
src/site.webmanifest Normal file
View File

@ -0,0 +1,19 @@
{
"name": "Wanderhome Quiz",
"short_name": "Wanderhome Quiz",
"icons": [
{
"src": "img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#1e441e",
"background_color": "#3a5a40",
"display": "standalone"
}