Merge pull request #1650 from berkenbu/master

New game 'bee' (spelling bee), add game statistics to 'bordle'
pull/1656/head
Gordon Williams 2022-04-04 09:35:40 +01:00 committed by GitHub
commit bf470cd43f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 74864 additions and 7 deletions

36
apps/bee/README.md Normal file
View File

@ -0,0 +1,36 @@
# Spelling bee game
Word finding game inspired by the NYT spelling bee. Find as many words with 4 or more letters (must include the
letter at the center of the 'hive') as you can.
## Usage
- tap on letters to type out word
- swipe left to delete last letter
- swipe right to enter; the word will turn blue while it is being checked against the internal dictionary; once
checked, it will turn red if the word is invalid, does not contain the central letter or has been guessed before or
will turn green if it is a valid word; in the latter case, points will be awarded
- swipe down to shuffle the 6 outer letters
- swipe up to view a list of already guessed words; tap on any of them to return to the regular game.
## Scoring
The number of correctly guessed words is displayed on the bottom left, the score on the bottom right. A single point
is awarded for a 4 letter word, or the number of letters if longer. A pangram is a word that contains all 7 letters at
least once and yields an additional 7 points. Each game contains at least one pangram.
## Technical remarks
The game uses an internal dictionary consisting of a newline separated list of English words ('bee.words', using the '2of12inf' word list).
The dictionary is fairly large (~700kB of flash space) and thus requires appropriate space on the watch and will make installing the app somewhat
slow. Because of its size it cannot be compressed (heatshrink needs to hold the compressed/uncompressed data in memory).
In order to make checking the validity of a guessed word faster an index file ('bee_lindex.json') is installed with
the app that facilitates faster word lookups. This index file is specific to the dictionary file used. If one were to
replace the dictionary file with a different version (e.g. a different language) the index file has to be regenerated. The easiest
way to do so is to delete (via the Web IDE or the fileman app on the watch) the file 'bee_lindex.json' - it will be regenerated (and saved,
i.e. it only happens once) on app startup automatically, a process that takes roughly 30 seconds.
![Screenshot](./bee_screenshot.png)

1
apps/bee/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AE2JAAIKHnc7DyNPp4vRGAwuBGB4sBAAQvSGIovPFqYvHGAYvDGBYsGGhwvGGIQvEGBQnDMYhkNGBAvOvQABqyRTF5GJr4wLFwQACX6IwLsowJLYMrldVGAQvTsoADGBITD0YvDldPF6n+F4gyGGAdP5nMF4KKBGDJZDGI7EBcoOiGAK7DGAQvYRogxEr1Pp9VMAiSBBILBWeJIxCromBMAQwDAAZfTGBQyCxOCGAIvBGIV/F7AwMAAOIp95GAYACFqoyQMAIwGF7QADEQd5FgIADqvGF8DnEAAIvFGIWjF8CFE0QwHAAQudAAK0EGBQuecw3GqpemYIxiCGIa8cF4wwHdTwvJp9/F82jGA9VMQovf5jkHGIwvg4wvIAAgvg5miF9wwNF8QABF9QwF0YuoF4oxCqoulGBAAB42i0QvjGBPMF0gwIFswwHF1IA/AH4A/AH4AL"))

BIN
apps/bee/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

193
apps/bee/bee.app.js Normal file
View File

@ -0,0 +1,193 @@
const S = require("Storage");
var letters = [];
var letterIdx = [];
var centers = [];
var word = '';
var foundWords = [];
var score = 0;
var intervalID = -1;
function prepareLetterIdx () {
"compile"
var li = [0];
if (S.read("bee_lindex.json")!==undefined) li = S.readJSON("bee_lindex.json"); // check for cached index
else {
for (var i=1; i<26; ++i) {
var prefix = String.fromCharCode(97+i%26);
console.log(prefix);
li.push(S.read('bee.words').indexOf("\n"+prefix, li[i-1])+1);
}
li.push(S.read('bee.words').length);
S.writeJSON("bee_lindex.json", li);
}
for (var i=0; i<26; ++i) letterIdx[i] = S.read("bee.words", li[i], li[i+1]-li[i]);
}
function findWord (w) {
"compile"
var ci = w.charCodeAt(0)-97;
var f = letterIdx[ci].indexOf(w);
if (f>=0 && letterIdx[ci][f+w.length]=="\n") return true;
return false;
}
function isPangram(w) {
var ltrs = '';
for (var i=0; i<w.length; ++i) if (ltrs.indexOf(w[i])===-1) ltrs += w[i];
return ltrs.length==7;
}
function checkWord (w) {
if (w.indexOf(String.fromCharCode(97+letters[0]))==-1) return false; // does it contain central letter?
if (foundWords.indexOf(w)>=0) return false; // already found
if (findWord(w)) {
foundWords.push(w);
if (w.length==4) score++;
else score += w.length;
if (isPangram(w)) score += 7;
return true;
}
return false;
}
function getHexPoly(cx, cy, r, a) {
var p = [];
for (var i=0; i<6; ++i) p.push(cx+r*Math.sin((i+a)/3*Math.PI), cy+r*Math.cos((i+a)/3*Math.PI));
return p;
}
function drawHive() {
w = g.getWidth();
h = g.getHeight();
const R = w/3.3;
centers = getHexPoly(w/2, h/2+10, R, 0);
centers.push(w/2, h/2+10);
g.clear();
g.setFont("Vector", w/7).setFontAlign(0, 0, 0);
g.setColor(g.theme.fg);
for (var i=0; i<6; ++i) {
g.drawPoly(getHexPoly(centers[2*i], centers[2*i+1], 0.9*R/Math.sqrt(3), 0.5), {closed:true});
g.drawString(String.fromCharCode(65+letters[i+1]), centers[2*i]+2, centers[2*i+1]+2);
}
g.setColor(1, 1, 0).fillPoly(getHexPoly(w/2, h/2+10, 0.9*R/Math.sqrt(3), 0.5));
g.setColor(0).drawString(String.fromCharCode(65+letters[0]), w/2+2, h/2+10+2);
}
function shuffleLetters(qAll) {
for (var i=letters.length-1; i > 0; i--) {
var j = (1-qAll) + Math.floor(Math.random()*(i+qAll));
var temp = letters[i];
letters[i] = letters[j];
letters[j] = temp;
}
}
function pickLetters() {
var ltrs = "";
while (ltrs.length!==7) {
ltrs = [];
var j = Math.floor(26*Math.random());
var i = Math.floor((letterIdx[j].length-10)*Math.random());
while (letterIdx[j][i]!="\n" && i<letterIdx[j].length) ++i;
if (i<letterIdx[j].length-1) {
++i;
while (letterIdx[j][i]!=="\n") {
var c = letterIdx[j][i];
if (ltrs.indexOf(c)===-1) ltrs += c;
++i;
}
}
}
for (var i=0; i<7; ++i) letters.push(ltrs.charCodeAt(i)-97);
shuffleLetters(1);
}
function drawWord(c) {
g.clearRect(0, 0, g.getWidth()-1, 19);
g.setColor(c).setFont("Vector", 20).setFontAlign(0, 0, 0).drawString(word, g.getWidth()/2, 11).flip();
}
function touchHandler(e, x) {
var hex = 0;
var hex_d = 1e6;
for (var i=0; i<7; ++i) {
var d = (x.x-centers[2*i])*(x.x-centers[2*i]) + (x.y-centers[2*i+1])*(x.y-centers[2*i+1]);
if (d < hex_d) {
hex_d = d;
hex = i+1;
}
}
hex = hex%7;
if (word.length <= 15) word += String.fromCharCode(letters[hex]+65);
drawWord(g.theme.fg);
}
function drawScore() {
g.setColor(g.theme.fg).setFont("Vector", 20).setFontAlign(0, 0, 0);
g.clearRect(0, g.getHeight()-22, 60, g.getHeight()-1);
g.clearRect(g.getWidth()-60, g.getHeight()-22, g.getWidth(), g.getHeight()-1);
g.drawString(foundWords.length.toString(), 30, g.getHeight()-11);
g.drawString(score.toString(), g.getWidth()-30, g.getHeight()-11);
}
function wordFound (c) {
word = "";
drawWord(g.theme.fg);
drawScore();
clearInterval(intervalID);
intervalID = -1;
}
function swipeHandler(d, e) {
if (d==-1 && word.length>0) {
word = word.slice(0, -1);
drawWord(g.theme.fg);
}
if (d==1 && word.length>=4) {
drawWord("#00f");
drawWord((checkWord(word.toLowerCase()) ? "#0f0" : "#f00"));
if (intervalID===-1) intervalID = setInterval(wordFound, 800);
}
if (e===1) {
shuffleLetters(0);
drawHive();
drawScore();
drawWord(g.theme.fg);
}
if (e===-1 && foundWords.length>0) showWordList();
}
function showWordList() {
Bangle.removeListener("touch", touchHandler);
Bangle.removeListener("swipe", swipeHandler);
E.showScroller({
h : 20, c : foundWords.length,
draw : (idx, r) => {
g.clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1).setFont("6x8:2");
g.setColor(isPangram(foundWords[idx])?'#0f0':g.theme.fg).drawString(foundWords[idx].toUpperCase(),r.x+10,r.y+4);
},
select : (idx) => {
setInterval(()=> {
E.showScroller();
drawHive();
drawScore();
drawWord(g.theme.fg);
Bangle.on("touch", touchHandler);
Bangle.on("swipe", swipeHandler);
clearInterval();
}, 100);
}
});
}
prepareLetterIdx();
pickLetters();
drawHive();
drawScore();
Bangle.on("touch", touchHandler);
Bangle.on("swipe", swipeHandler);

1
apps/bee/bee_lindex.json Normal file
View File

@ -0,0 +1 @@
[0,41048,80445,152390,198606,228714,257919,279071,303726,337982,343582,348026,367246,404452,419780,438696,496250,499697,544600,624304,659085,680996,691270,708186,708341,709916,710883]

BIN
apps/bee/bee_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

74578
apps/bee/bee_words_2of12 Normal file

File diff suppressed because it is too large Load Diff

16
apps/bee/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{ "id": "bee",
"name": "Bee",
"shortName":"Bee",
"icon": "app.png",
"version":"0.01",
"description": "Spelling bee",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"tags": "game,text",
"storage": [
{"name":"bee.app.js","url":"bee.app.js"},
{"name":"bee.words","url":"bee_words_2of12"},
{"name":"bee_lindex.json","url":"bee_lindex.json"},
{"name":"bee.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -21,7 +21,8 @@ function buttonPushed(b) {
layout.bt1.bgCol = wordle.keyColors.Z||g.theme.bg;
layout.bt2.label = "<del>";
layout.bt4.label = "<ent>";
layout.bt3.label = layout.bt5.label = " ";
layout.bt3.label = " ";
layout.bt5.label = "<stat>";
layout.bt6.label = "<";
}
}
@ -30,6 +31,10 @@ function buttonPushed(b) {
if (b!=6) {
if ((keyStateIdx<=5 || b<=1) && inp.length<5) inp += String.fromCharCode(b+(keyStateIdx-1)*5+64);
else if (layout.input.label.length>0 && b==2) inp = inp.slice(0,-1);
if (keyStateIdx==6 && b==5) {
wordle.drawStats();
return;
}
layout.input.label = inp;
}
layout = getKeyLayout(inp);
@ -82,6 +87,7 @@ class Wordle {
this.word = this.words.slice(i, i+5).toUpperCase();
}
console.log(this.word);
this.stats = require("Storage").readJSON("bordlestats.json") || {'1':0, '2':0, '3':0, '4':0, '5':0, '6':0, 'p':0, 'w':0, 's':0, 'ms':0};
}
render(clear) {
h = g.getHeight();
@ -109,7 +115,7 @@ class Wordle {
layout = getKeyLayout("");
wordle.render(true);
});
return 3;
return 1;
}
this.guesses.push(w);
this.nGuesses++;
@ -130,13 +136,39 @@ class Wordle {
this.guessColors[this.nGuesses].push(col);
}
if (correct==5) {
E.showAlert("The word is\n"+this.word, "You won in "+(this.nGuesses+1)+" guesses!").then(function(){load();});
return 1;
}
if (this.nGuesses==5) {
E.showAlert("The word was\n"+this.word, "You lost!").then(function(){load();});
E.showAlert("The word is\n"+this.word, "You won in "+(this.nGuesses+1)+" guesses!").then(function(){
wordle.stats['p']++; wordle.stats['w']++; wordle.stats['s']++; wordle.stats[wordle.nGuesses+1]++;
if (wordle.stats['s']>wordle.stats['ms']) wordle.stats['ms'] = wordle.stats['s'];
require("Storage").writeJSON("bordlestats.json", wordle.stats);
wordle.drawStats();
});
return 2;
}
if (this.nGuesses==5) {
E.showAlert("The word was\n"+this.word, "You lost!").then(function(){
wordle.stats['p']++; wordle.stats['s'] = 0;
require("Storage").writeJSON("bordlestats.json", wordle.stats);
wordle.drawStats();
});
return 3;
}
}
drawStats() {
E.showMessage(" ", "Statistics");
var max = 1;
for (i=1; i<=6; ++i) if (max<this.stats[i]) max = this.stats[i];
var h = g.getHeight();
var w = g.getWidth();
g.setColor('#00f').setFontVector((h-40)/8).setFontAlign(-1, 0, 0);
for (i=1; i<=6; ++i) {
tw = this.stats[i]*(w-24)/max;
g.setColor("#00f").fillRect(20, 52+(i-1)*(h-52)/6+2, 20+tw, 52+i*(h-52)/6-2);
g.setColor("#fff").drawString(i.toString(), 1, 52+(i-0.5)*(h-52)/6);
g.drawString(this.stats[i].toString(), tw>20 ? 25 : 25+tw, 52+(i-0.5)*(h-52)/6);
}
g.setFontVector((h-40)/9).setColor("#fff").drawString("P:"+this.stats["p"]+" W:"+this.stats["w"]+" S:"+this.stats["s"]+" M:"+this.stats["ms"], 4, 34);
Bangle.setUI();
Bangle.on("touch", (e) => { load(); });
}
}