first commit, working game, some work to do yet

This commit is contained in:
Sander Hautvast 2020-02-17 22:28:46 +01:00
commit 3bc2b0fe18
17 changed files with 15694 additions and 0 deletions

17
.eslintrc.js Normal file
View file

@ -0,0 +1,17 @@
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
}
};

113
.gitignore vendored Normal file
View file

@ -0,0 +1,113 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# intellij
.idea
*.iml
# apple
.DS_Store

4
README.md Normal file
View file

@ -0,0 +1,4 @@
* remake of word collect (app) without the ads
* no demo yet
* static html/javascript
* pretty hard!

25
index.html Normal file
View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<meta name="viewport" content="minimal-ui, width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<html>
<head>
<title>WordCollector</title>
</head>
<body>
<canvas id="canvas" draggable="false">
Your browser does not support the canvas element.
</canvas>
<div class="points"><img class="medal" src="src/img/medal.png"><span id="points">0</span></div>
<div id="slots">
</div>
<div id="guess" class="guess"></div>
<div class="hint"><img id="hint" alt="hint" src="src/img/hint.png" width="50px"></div>
<div class="reload"><img id="reload" alt="shuffle" src="src/img/reload.png" width="50px"></div>
<div id="characters"></div>
<script src="bundle.js"></script>
</body>
</html>

7729
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "wordcollector",
"version": "0.0.0",
"private": true,
"main": "src/js/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"watch": "webpack --watch",
"start": "webpack-dev-server --open --host 0.0.0.0 --port 80"
},
"dependencies": {},
"devDependencies": {
"css-loader": "^3.4.2",
"eslint": "^6.8.0",
"eslint-loader": "^3.0.3",
"fabric": "^4.0.0-beta.6",
"file-loader": "^5.0.2",
"jquery": "^3.4.1",
"style-loader": "^1.1.3",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
}
}

154
src/css/style.css Normal file
View file

@ -0,0 +1,154 @@
html {
margin: 0;
height: 100%;
overflow: hidden;
user-select: none;
}
body {
font-family: 'Quicksand', serif;
font-weight: 500;
font-style: normal;
background: #53a600;
padding: 0;
margin: 0;
height: 100%;
overflow: hidden;
flex-flow: row-reverse wrap;
}
.canvas-container {
position: absolute;
left: 0;
top: 50%;
width: 100%;
height: 100%;
z-index: 1;
}
.character-tile {
display: inline-block;
vertical-align: top;
transform: translate(0, -15%);
}
.medal {
height: 1em;
transform: translate(-12px, 2px);
}
.points {
position: absolute;
right: 5%;
top: 1%;
flex-flow: row-reverse wrap;
color: yellow;
background: black;
border: #dab107 2px solid;
border-radius: 4px;
padding-left: 3px;
padding-right: 3px;
font-size: large;
}
#slots {
top: 35px;
width: 90%;
margin: 5px;
display: flex;
flex-flow: row-reverse wrap;
border: #f5f1bc solid 15px;
border-radius: 15px;
background: #f5f1bc;
position: absolute;
horiz-align: center;
}
.wordslot {
position: relative;
padding: 10px;
display: flex;
}
.hidden {
color: brown;
}
.revealed {
color: white;
}
.characterslot {
vertical-align: center;
text-align: center;
width: 1.5em;
height: 1.5em;
background: brown;
border: black solid .5px;
border-radius: 3px;
}
.tile {
position: absolute;
background: url("../img/wood.jpg");
width: 18vw;
height: 10vh;
max-width: 75px;
text-align: center;
color: rgb(99, 28, 28);
border: solid beige 1px;
border-radius: 5px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 1), 0 0 40px rgba(255, 255, 0, 0.2) inset;
font-size: 10vh;
font-weight: bold;
}
.guess {
padding-left: 5px;
padding-right: 5px;
visibility: hidden;
background: #f5f1bc;
position: absolute;
top: 37%;
left: 40%;
display: inline-block;
height: 11vh;
text-align: center;
color: black;
border: solid beige 2px;
border-radius: 5px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 1), 0 0 40px rgba(100,100,100, 0.1) inset;
font-size: 7vh;
font-weight: bold;
}
.shown {
visibility: visible;
}
.selected {
border-width: 5px;
}
@font-face {
font-family: 'Quicksand';
font-style: normal;
font-weight: 500;
src: url(../font/quicksand.woff) format('woff');
}
.reload {
padding: 5px;
position: absolute;
top: 40%;
right: 5%;
border-radius: 10px;
}
.hint {
padding: 5px;
position: absolute;
top: 40%;
left: 5%;
border-radius: 10px;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3511
src/data/wordcombis6.json Normal file

File diff suppressed because it is too large Load diff

BIN
src/font/quicksand.woff Normal file

Binary file not shown.

BIN
src/img/hint.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/img/medal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
src/img/reload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
src/img/wood.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

278
src/js/index.js Normal file
View file

@ -0,0 +1,278 @@
import $ from 'jquery';
import '../css/style.css';
import {fabric} from 'fabric';
import data from '../data/wordcombis6.json';
new function () {
const characterTiles = document.getElementsByClassName('tile'); //
let lines = []; //lines connecting the letters
let activeLine; //the line that moves with the pointer
let screenMiddle;
let words, // the words to guess
characters, // the letters to choose from
guess = "", // the current guess
points = 0;
const canvas = new fabric.Canvas('canvas', {selection: false});
resize();
registerEvents();
start();
function start() {
// get new word
let challenge = data[Math.floor(Math.random() * data.length)];
words = challenge.words;
// max ten words to guess
// TODO put the rest in the extra-word list for bonus points
// TODO make sure words of all lengths are guaranteed to be in the list
while (words.length > 10) {
words.splice(Math.random() * Math.floor(words.length), 1);
}
showEmptySlots(); // this is where the guessed words appear
characters = shuffleLetters(challenge.chars); // random order, otherwise the longest word is to easily visible
layoutLetterTiles(); // put them on screen
}
// boxes for the guessed words
// TODO nice columnar layout
function showEmptySlots() {
let slots = $("#slots");
slots.children().remove();
for (let i = words.length - 1; i >= 0; i--) {
let word = words[i];
let wordSlot = $("<div class='wordslot hidden' id='" + word + "'>");
slots.append(wordSlot);
for (let j = 0; j < word.length; j++) {
wordSlot.append($("<div class='characterslot'>").append(word.charAt(j).toUpperCase()));
}
}
}
// put all letter tiles in a nice circle
function layoutLetterTiles() {
let width = $(window).width();
let height = $(window).height();
let center_x = width / 2.4;
let center_y = height / 2.2;
let angleBetweenChars = 2 * Math.PI / characters.length;
//remove any characters that were there before
$("#characters div").remove();
// layout new ones in a circle
for (let i = 0; i < characters.length; i++) {
let radius_x = Math.min((width / 4), 150); // not too wide on big screens
let x = center_x - radius_x * Math.cos(i * angleBetweenChars) + Math.random();
let y = center_y * 1.5 - (height / 6) * Math.sin(i * angleBetweenChars) + Math.random() * 20 - 20;
// make visible
let span = $("<span class='character-tile' id='" + i + "-" + characters[i] + "'>");
span.append(characters[i].toUpperCase());
let div = $("<div class='tile' style='top:" + y + "px;left:" + x + "px;transform: rotate(" + (Math.random() * 8 - 4) + "deg)'>");
div.append(span);
$("#characters").append(div);
}
}
function registerEvents() {
$(window).resize(function () {
layoutLetterTiles();
});
canvas.on('mouse:down', event => {
if (!activeLine) {
addLineToCanvas(event);
updatePointer(event);
} else {
reset();
}
});
canvas.on('mouse:up', () => {
checkGuess();
reset();
});
canvas.on('mouse:move', updatePointer);
$("#reload").on("click", () => {
characters = shuffleLetters(characters);
layoutLetterTiles();
});
$("#hint").on("click", () => {
showHint();
});
}
function checkGuess() {
if (guess.length > 0) {
// if the guess is correct, the word will light up
$("#" + guess).attr("class", "wordslot revealed");
// check if all words are found
for (let i = 0; i < words.length; i++) {
if (guess === words[i]) {
//update points
points += guess.length;
$("#points").text(points);
//remove word from words to guess
words.splice(words.indexOf(guess), 1);
if (words.length === 0) {
start(); //restart
}
}
}
}
}
function updatePointer(event) {
if (activeLine) {
let pointer = canvas.getPointer(event.e, false);
//move current line to mouse pointer
activeLine.set({x2: pointer.x, y2: pointer.y});
// check for hits
for (let ix = 0; ix < characterTiles.length; ix++) {
let tile = characterTiles[ix];
if (isHit(tile, pointer)) {
registerHit(tile, event);
}
}
}
canvas.renderAll();
}
function addLineToCanvas(event) {
let points = [event.e.layerX, event.e.layerY, event.e.layerX, event.e.layerY];
let line = new fabric.Line(points, {
strokeWidth: 4,
stroke: '#ffffff',
opacity: 0.5,
hasBorders: false,
hasControls: false,
selectable: false,
evented: false
});
activeLine = line; // the new line becomes the line that moves with the mouse/finger
lines.push(line); // register, to be able to remove later
canvas.add(line); // make visible on canvas
}
function reset() {
// remove visible lines
for (let i = 0; i < lines.length; i++) {
canvas.remove(lines[i]);
}
// empty the lines in memory
lines = [];
// reset the activeline, so the gui goes into it's initial state
activeLine = null;
// reset all tiles
for (let i = 0; i < characterTiles.length; i++) {
characterTiles[i].setAttribute("class", "tile");
}
// reset guess
let guessContainer = $("#guess");
guessContainer.children().remove();
guessContainer.attr("class", "guess");
guess = "";
}
// simple collision detection
function isHit(tile, pointer) {
let tileX = parseInt(tile.style.left.valueOf());
let tileY = parseInt(tile.style.top.valueOf());
let dx = pointer.x - tileX;
let dy = pointer.y + screenMiddle - tileY;
return dx > 10 && dx < 80 && dy > 10 && dy < 80;
}
function registerHit(tile, event) {
if (notHitAlready(tile)) {
updateViewToSelected(tile);
// create a new line from here
addLineToCanvas(event);
// extract the character on the selected tile
let character = tile.firstChild.id.substring(2);
// add it to the current guess
guess = guess + character;
// and update the guess view
let guessContainer = $("#guess");
let guessCharContainer = $("<span>");
guessCharContainer.text(character.toUpperCase());
guessCharContainer.appendTo(guessContainer);
guessContainer.attr("class", "guess shown");
// update guess view position on screen
let newSize = ($(window).width() - 20) / 2 - guess.length * 17;
guessContainer.css("left", newSize + "px");
}
}
function updateViewToSelected(tile) {
tile.setAttribute("class", "tile selected");
}
function notHitAlready(tile) {
return tile.getAttribute("class") === "tile";
}
function resize() {
canvas.setWidth($(window).width());
screenMiddle = $(window).height() / 2;
canvas.setHeight(screenMiddle);
}
function shuffleLetters(array) {
let j, x;
for (let i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = array[i];
array[i] = array[j];
array[j] = x;
}
return array;
}
function showHint() {
let found = false;
let count = 0; //100 tries
while (!found && count++ < 100) {
let word = words[Math.floor(Math.random() * words.length)];
let letter = word[Math.floor(Math.random() * word.length)].toUpperCase();
let slot = $("#" + word).children();
for (let i = 0; i < word.length; i++) {
if (slot[i].firstChild.nodeValue === letter && slot[i].getAttribute("class") === "characterslot") {
slot[i].setAttribute("class", "characterslot revealed");
found = true;
break;
}
}
}
}
}();

42
webpack.config.js Normal file
View file

@ -0,0 +1,42 @@
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/js/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
emitError: true,
},
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
],
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader',
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader',
],
},
],
},
};