summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--index.html64
-rw-r--r--static/EBGaramond-Italic-VariableFont_wght.ttfbin0 -> 829752 bytes
-rw-r--r--static/EBGaramond-VariableFont_wght.ttfbin0 -> 897728 bytes
-rw-r--r--static/OFL.txt93
-rw-r--r--static/favicon.icobin0 -> 318 bytes
-rw-r--r--static/poems.js46
-rw-r--r--static/script.js192
-rw-r--r--static/styles.css141
9 files changed, 537 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8bc1b69
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Prosodyle
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..0ede797
--- /dev/null
+++ b/index.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Prosodyle</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" type="text/css" href="/static/styles.css">
+ <link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico">
+ </head>
+ <body>
+ <div class="container">
+ <div>
+ <a href="/" class="title">Prosodyle</a><span>@</span><a href="https://cyfraeviolae.org">cyfraeviolae.org</a>
+ <span class="sep">|</span>
+ <a href="https://cyfraeviolae.org/git/prosodyle">source code</a>
+ </div>
+ <p>
+ The dithyrambic sorcerer Roseacrucis has breached the Library&rsquo;s security measures and encrypted its
+ treasured poetry collections. As our newest acolyte, your task is to decode one secret line of poetry each day.
+ </p>
+ <p>
+ For the ransom price of one new line with the same metrical and syllabic structure as today&rsquo;s secret
+ line, Roseacrucis will reveal which letters are in the right place 🟩, in the right word but in a different
+ place 🟨, or not in the word at all ⬜.
+ </p>
+ <noscript>Sorry, JavaScript is required to play Prosodyle.</noscript>
+ <form id="game" action="javascript:void(0);" method="none">
+ <p>
+ Write a line of poetry. Syllables in dark boxes should be stressed.
+ </p>
+ <div id="entry"></div>
+ <div id="btns">
+ <button id="check" type="submit">Check</button>
+ <button id="clear">Clear</button>
+ </div>
+ <hr>
+ <div id="win" style="display: none;">
+ <p>
+ Victory is yours! Today&rsquo;s secret line was in
+ <strong id="meter"></strong>:
+ </p>
+ <pre id="ctx" class="poetry"></pre>
+ <p>
+ But our troubles are not over. Most of the Library&rsquo;s poetry
+ remains encrypted, and we need your help to decode a new line
+ tomorrow.
+ </p>
+ <p>
+ In the meantime, you may share your results with your fellow acolytes,
+ adding punctuation as desired.
+ </p>
+ <div>
+ <textarea cols="80" rows="6" id="share"></textarea>
+ </div>
+ <button id="copy">Copy</button>
+ <hr>
+ </div>
+ <p id="guesses"></p>
+ </form>
+ </div>
+ <script src="/static/poems.js"></script>
+ <script src="/static/script.js"></script>
+ </body>
+</html>
diff --git a/static/EBGaramond-Italic-VariableFont_wght.ttf b/static/EBGaramond-Italic-VariableFont_wght.ttf
new file mode 100644
index 0000000..fa31b71
--- /dev/null
+++ b/static/EBGaramond-Italic-VariableFont_wght.ttf
Binary files differ
diff --git a/static/EBGaramond-VariableFont_wght.ttf b/static/EBGaramond-VariableFont_wght.ttf
new file mode 100644
index 0000000..123d5dd
--- /dev/null
+++ b/static/EBGaramond-VariableFont_wght.ttf
Binary files differ
diff --git a/static/OFL.txt b/static/OFL.txt
new file mode 100644
index 0000000..1ba1596
--- /dev/null
+++ b/static/OFL.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..bb88dc0
--- /dev/null
+++ b/static/favicon.ico
Binary files differ
diff --git a/static/poems.js b/static/poems.js
new file mode 100644
index 0000000..fd5d0bd
--- /dev/null
+++ b/static/poems.js
@@ -0,0 +1,46 @@
+var challenges = [
+ {
+ "line": [["In", "/fin", "ite"], ["/wrath"], ["and"], ["/in", "fin", "/ite"], ["des", "/pair"]],
+ "title": "Book IV",
+ "collection": "Paradise Lost",
+ "author": "John Milton",
+ "meter": "iambic pentameter",
+ "ctx": `Me miserable! which way shall I flie
+Infinite wrauth, and infinite despaire?
+Which way I flie is Hell; my self am Hell;
+And in the lowest deep a lower deep
+Still threatning to devour me opens wide,
+To which the Hell I suffer seems a Heav'n.`,
+ "link": "https://milton.host.dartmouth.edu/reading_room/pl/book_4/text.shtml",
+ },
+ {
+ "line": [["For"], ["the"], ["/An", "gel"], ["of"], ["/Death"], ["spread"], ["his"], ["/wings"], ["on"], ["the"], ["/blast"]],
+ "title": "The Destruction of Sennacherib",
+ "collection": "Hebrew Melodies",
+ "author": "Lord Byron",
+ "meter": "anapestic tetrameter",
+ "ctx": `For the Angel of Death spread his wings on the blast,
+And breathed in the face of the foe as he passed;
+And the eyes of the sleepers waxed deadly and chill,
+And their hearts but once heaved, and for ever grew still!`,
+ "link": "https://www.poetryfoundation.org/poems/43827/the-destruction-of-sennacherib",
+ },
+ {
+ "line": [["A"], ["/pre", "sence"], ["/that"], ["dis", "/turbs"], ["me"], ["/with"], ["the"], ["/joy"]],
+ "title": "Lines written a few miles above Tintern Abbey",
+ "collection": "Lyrical Ballads",
+ "author": "William Wordsworth",
+ "meter": "iambic pentameter",
+ "ctx": `And I have felt
+A presence that disturbs me with the joy
+Of elevated thoughts; a sense sublime
+Of something far more deeply interfused,
+Whose dwelling is the light of setting suns,
+And the round ocean, and the living air,
+And the blue sky, and in the mind of man,
+A motion and a spirit, that impels
+All thinking things, all objects of all thought,
+And rolls through all things.`,
+ "link": "https://www.gutenberg.org/files/9622/9622-h/9622-h.htm#poem23",
+ },
+]
diff --git a/static/script.js b/static/script.js
new file mode 100644
index 0000000..552da72
--- /dev/null
+++ b/static/script.js
@@ -0,0 +1,192 @@
+"use strict";
+
+var entryEl = document.getElementById("entry")
+var guessesEl = document.getElementById("guesses")
+var winEl = document.getElementById("win")
+
+function renderWord(word, wordIdx, guess, score, offset) {
+ var els = []
+ var c = 0;
+ for (var syllable of word) {
+ var sylEls = []
+
+ var shouldStress = syllable[0] == "/";
+ if (shouldStress) {
+ syllable = syllable.slice(1)
+ }
+ var stress = shouldStress ? "stress" : ""
+ for (var _ in syllable) {
+ var charclass = ''
+ var able = ''
+ if (score) {
+ if (score.greens[c]) {
+ charclass += 'green'
+ } else if (score.yellows[c]) {
+ charclass += 'yellow'
+ } else if (score.greys[c]) {
+ charclass += 'grey'
+ }
+ able = 'disabled'
+ }
+ var val = guess ? guess[wordIdx][c] : ''
+ var typ = guess ? 'solbox' : 'entrybox'
+ sylEls.push(`<input data-word="${wordIdx}" data-character="${c}" data-offset="${c+offset}" class="box ${typ} ${charclass}" type="text" maxlength="1" required title="A-Z only" value="${val}" pattern="[A-Za-z]" ${able}></input>`)
+ c++;
+ }
+
+ els.push(`<div class="syllable ` + stress + `">` + sylEls.join('') + `</div>`)
+ }
+
+ return `<div class="word">` + els.join(`<div class="syllable-sep">&middot;</div>`) + `</div>`
+}
+
+function renderLine(line, guess, scores) {
+ var els = []
+ var offset = 0;
+ for (var wordIdx in line) {
+ var word = line[wordIdx]
+ els.push(renderWord(word, wordIdx, guess, scores && scores[wordIdx], offset))
+ offset += word.join('').replaceAll('/', '').length
+ console.log(word, word.join('').replaceAll('/', '').length)
+ }
+ return `<div class="line">` + els.join('') + `</div>`
+}
+
+function consumeInput() {
+ var els = Array.from(document.getElementsByClassName("entrybox"));
+ els.sort((x, y) => {
+ var xword = x.getAttribute('data-word')
+ var xchar = x.getAttribute('data-character')
+ var yword = y.getAttribute('data-word')
+ var ychar = y.getAttribute('data-character')
+
+ if (xword != yword) {
+ return xword - yword;
+ }
+ return xchar - ychar;
+ })
+ var words = {}
+ for (var el of els) {
+ words[el.getAttribute('data-word')] = (words[el.getAttribute('data-word')] || '') +
+ el.value.toUpperCase()
+ }
+ return words
+}
+
+function scoreLine(guess, answer) {
+ var scores = []
+ for (var idx in guess) {
+ var guessword = guess[idx]
+ var answerword = answer[idx].join('').replaceAll('/', '').toUpperCase()
+ scores.push(scoreWord(guessword, answerword))
+ }
+ return scores
+}
+
+function scoreWord(guess, answer) {
+ var greens = {}
+ var yellows = {}
+ var greys = {}
+ var counts = {}
+ var placed = {}
+ for (var idx in guess) {
+ if (guess[idx] == answer[idx]) {
+ greens[idx] = 1
+ placed[answer[idx]] = (placed[answer[idx]] || 0) + 1
+ }
+ counts[answer[idx]] = (counts[answer[idx]] || 0) + 1
+ }
+ for (var idx in guess) {
+ if (greens[idx]) {
+ continue
+ }
+ if ((placed[guess[idx]] || 0) < counts[guess[idx]]) {
+ yellows[idx] = 1
+ placed[guess[idx]] = (placed[guess[idx]] || 0) + 1;
+ } else {
+ greys[idx] = 1
+ }
+ }
+ return {greens: greens, yellows: yellows, greys: greys}
+}
+
+document.getElementById('clear').onclick = function(e) {
+ e.preventDefault();
+ for (var el of document.getElementsByClassName('entrybox')) {
+ el.value = ''
+ }
+ document.querySelector(`[data-offset="0"]`).focus();
+}
+
+document.getElementById('game').addEventListener('submit', function(e) {
+ var guess = consumeInput()
+ guesses.push(guess)
+ var scores = scoreLine(guess, challenge.line)
+ var linehtml = renderLine(challenge.line, guess, scores)
+ guessesEl.innerHTML = linehtml + guessesEl.innerHTML
+ var win = true;
+ for (var score of scores) {
+ if (Object.keys(score.yellows).length || Object.keys(score.greys).length) {
+ win = false;
+ }
+ }
+ if (win) {
+ entryEl.innerHTML = linehtml
+ winGame(challenge)
+ }
+})
+
+function winGame(challenge) {
+ winEl.style = 'display: block;'
+ document.getElementById('btns').style = 'display: none;'
+ document.getElementById('meter').innerText = challenge.meter
+ document.getElementById('ctx').innerHTML = challenge.ctx.replaceAll(/^(.*)/gm, '\t$1') + `\n\t<span class="byline">&mdash;<a href="${challenge.link}">${challenge.title}</a>, ${challenge.collection}, ${challenge.author}</span>`
+ var date = new Date().toISOString().slice(0, 10)
+ var firstguess = Object.values(guesses[0]).join(' ')
+ document.getElementById('share').value = `I solved the ${date} Prosodyle at cyfraeviolae.org/prosodyle. My first guess was: "${firstguess}".`
+}
+
+document.getElementById('entry').addEventListener('keydown', function(e) {
+ if (!Array.from(e.target.classList).includes('entrybox')) {
+ return;
+ }
+ if (e.ctrlKey || e.altKey || (e.key.length != 1 && e.key != 'Backspace')) {
+ return;
+ }
+ e.preventDefault();
+ var offset = parseInt(e.target.getAttribute('data-offset'))
+ var el;
+ if (e.key == 'Backspace') {
+ e.target.value = '';
+ el = document.querySelector(`[data-offset="${offset-1}"]`);
+ } else {
+ e.target.value = e.key;
+ el = document.querySelector(`[data-offset="${offset+1}"]`);
+ }
+ if (el) {
+ el.focus();
+ }
+})
+
+function getDailyChallenge() {
+ // https://stackoverflow.com/a/8619946
+ var now = new Date();
+ var start = new Date(now.getFullYear(), 0, 0);
+ var diff = now - start;
+ var oneDay = 1000 * 60 * 60 * 24;
+ var day = Math.floor(diff / oneDay);
+ // end snippet
+ return challenges[day-56]
+}
+
+document.getElementById('copy').addEventListener('click', function(e) {
+ e.preventDefault();
+ document.getElementById('share').select()
+ document.execCommand('copy');
+})
+
+
+var guesses = []
+var challenge = getDailyChallenge()
+entryEl.innerHTML = renderLine(challenge.line)
+document.querySelector(`[data-offset="0"]`).focus();
diff --git a/static/styles.css b/static/styles.css
new file mode 100644
index 0000000..96b9109
--- /dev/null
+++ b/static/styles.css
@@ -0,0 +1,141 @@
+@font-face {
+ font-family: EBGaramond;
+ src: url(/static/EBGaramond-VariableFont_wght.ttf) format('woff2-variations');
+ font-style: normal;
+}
+
+@font-face {
+ font-family: EBGaramond;
+ src: url(/static/EBGaramond-Italic-VariableFont_wght.ttf) format('woff2-variations');
+ font-style: italic;
+}
+
+body {
+ background: #fdf3f3;
+ color: DarkSlateGrey;
+ font-family: EBGaramond, serif;
+ font-size: large;
+}
+
+a {
+ color: #1a97bf;
+}
+
+a:hover {
+ color: #075d77;
+}
+
+.container {
+ margin: 1em;
+ max-width: 40em;
+}
+
+.title {
+ letter-spacing: -0.5px;
+ font-weight: bold;
+}
+
+div {
+ margin-bottom: 10px;
+}
+
+form {
+ border: 1px DarkSlateGrey solid;
+ padding: 10px;
+ padding-left: 25px;
+}
+
+.sep {
+ margin-left: 6px;
+ margin-right: 6px;
+}
+
+.ingredient {
+ display: inline-block;
+ margin-right: 5px;
+ margin-bottom: 5px;
+}
+
+button {
+ margin-right: 10px;
+}
+
+.box {
+ width: 35px;
+ height: 35px;
+ text-align: center;
+ font-weight: bold;
+ font-size: 22px;
+ border: 2px lightgrey solid;
+ margin-right: 0px;
+ margin-left: 0px;
+ text-transform: uppercase;
+ color: black;
+ box-sizing: border-box;
+ caret-color: transparent;
+ outline:0px none transparent;
+ /* appearance: none; */
+ /* -moz-appearance: none; */
+}
+.box:focus {
+ background-color: #fff079;
+}
+.box::selection {
+ color: none;
+ background: none;
+}
+.syllable .box:not(:first-child) {
+ margin-left: -1px;
+}
+.syllable .box:not(:last-child) {
+ margin-right: -1px;
+}
+
+.syllable {
+ display: inline;
+}
+.syllable-sep {
+ display: inline;
+ font-size: 30px;
+ margin: 1px;
+}
+.word {
+ display: inline-block;
+ margin-right: 40px;
+ margin-bottom: 7px;
+}
+
+.stress>.box {
+ border-color: darkslategrey;
+ /* background-color: lightcyan; */
+}
+
+.green {
+ background-color: rgb(106, 170, 100);
+ color: white;
+ border: 0;
+}
+.yellow {
+ background-color: rgb(201, 180, 88);
+ color: white;
+ border: 0;
+}
+.grey {
+ background-color: rgb(120, 124, 126);
+ color: white;
+ border: 0;
+}
+
+hr {
+ border: .1px DarkSlateGrey dashed;
+ margin-top: 20px;
+}
+
+.poetry {
+ font-family: EBGaramond;
+}
+
+.byline {
+ font-style: italic;
+ font-size: 75%;
+}