Skip to content

Instantly share code, notes, and snippets.

@scriptype
Created February 26, 2017 21:05
Show Gist options
  • Save scriptype/95cb8b0afcc693df703ef26dbd61d489 to your computer and use it in GitHub Desktop.
Save scriptype/95cb8b0afcc693df703ef26dbd61d489 to your computer and use it in GitHub Desktop.
MpYKVq
<div class="game">
<h1 class="game__title">Satisfy the inputs</h1>
<div class="game__screen" id="game-screen"></div>
<div class="game__hud">
<div class="game__lives" id="game-lives"></div>
<div class="game__score" id="game-score"></div>
</div>
</div>
define('$.markupToDOM', _ => markup => {
var parent = document.createElement('div')
return (function() {
parent.innerHTML = markup.trim()
return parent.removeChild(parent.firstChild)
})()
})
define('State', _ => (data, onChange) => Object.assign(data, {
update(newState) {
Object.assign(this, newState)
onChange(newState)
}
}))
define('$', ['$.markupToDOM'], markupToDOM => selector => {
var bareSelector = selector.trim()
var firstChar = bareSelector.charAt(0)
if (firstChar == '#') {
return document.getElementById(selector.slice(1))
} else if (firstChar == '<') {
return markupToDOM(bareSelector)
}
return document.querySelectorAll(selector)
})
define('Game.Globals.SeedNoise', _ => _ => {
noise.seed(Math.random())
})
define('Game.Objects.Text', ['$'], $ => _ => ({
CLASS_NAME: 'text-object',
template(whatToType, question) {
return $(`
<label class="${ this.CLASS_NAME }">
<span class="${ this.CLASS_NAME }__title">
${ question ? 'question: ' : 'type: '}
${ question || whatToType }
</span>
<input
type="text"
class="${ this.CLASS_NAME }__input" />
</label>
`)
},
create({ question, whatToType, satisfactionCB }) {
this.state = {
isValid: true
}
this.$el = this.template(whatToType, question)
this.question = question
this.whatToType = whatToType
this.satisfactionCB = satisfactionCB
return this
},
render($container) {
$container.appendChild(this.$el)
this.$el.addEventListener('input', e => this.onInput(e))
return this
},
destroy() {
this.$el.remove()
return this
},
onInput(event) {
var value = event.target.value
if (value === this.whatToType) {
this.satisfactionCB.call(this)
} else {
this.validate(value)
}
},
validate(value) {
var wasValid = this.state.isValid
var isValid = this.whatToType.includes(value)
// If validity changed since last time
// XOR: True only if the values are different
if (wasValid ^ isValid) {
this.$el.classList.toggle(this.CLASS_NAME + '--invalid')
this.state.isValid = !wasValid
}
}
}))
define('Game.Atoms.Button', ['$'], $ => _ => ({
CLASS_NAME: 'button-object',
template(buttonText) {
return $(`
<button
type="button"
class="${ this.CLASS_NAME }">
${ buttonText }
</button>
`)
},
create({ buttonText='Click me', onClickCB }) {
this.$el = this.template(buttonText)
this.buttonText = buttonText
this.onClickCB = onClickCB
return this
},
render($container) {
$container.appendChild(this.$el)
this.$el.addEventListener('click', e => this.onClick(e))
return this
},
destroy() {
this.$el.remove()
return this
},
onClick() {
this.onClickCB.call(this)
}
}))
define('Game.Objects.Button', ['Game.Atoms.Button'], ButtonAtom => _ => ({
create(options) {
return ButtonAtom().create(Object.assign({}, options, {
onClickCB: options.satisfactionCB
}))
}
}))
define('Game.Atoms.Checkbox', ['$'], $ => _ => ({
CLASS_NAME: 'checkbox-object',
template(label, isGrouped) {
var mod = isGrouped ? this.CLASS_NAME + '--grouped' : ''
return $(`
<label class="${ this.CLASS_NAME } ${ mod }">
<input
type="checkbox"
class="${ this.CLASS_NAME }__check" />
${ !label ? '' : `
<span class="${ this.CLASS_NAME }__title">
${ label }
</span>
` }
</label>
`)
},
create({
isChecked = false,
label,
isGrouped,
onChangeCB
}) {
this.$el = this.template(label, isGrouped)
this.isChecked = isChecked
this.onChangeCB = onChangeCB
return this
},
render($container) {
$container.appendChild(this.$el)
if (this.isChecked) {
this.$el.querySelector('input').checked = true
}
this.$el.addEventListener('change', e => this.onChange(e))
return this
},
destroy() {
this.$el.remove()
return this
},
onChange(e) {
this.onChangeCB.call(this, e.target.checked)
}
}))
define('Game.Objects.Checkbox', ['Game.Atoms.Checkbox'], CheckboxAtom => _ => ({
create(options) {
return CheckboxAtom().create(Object.assign({}, options, {
onChangeCB: options.satisfactionCB
}))
}
}))
define('Game.Atoms.Radio', ['$'], $ => _ => ({
CLASS_NAME: 'radio-object',
template(name, isGrouped, label) {
var mod = isGrouped ? this.CLASS_NAME + '--grouped' : ''
return $(`
<label class="${ this.CLASS_NAME } ${ mod }">
<input
name="${ name }"
type="radio"
class="${ this.CLASS_NAME }__check" />
${ !label ? '' : `
<span class="${ this.CLASS_NAME }__title">
${ label }
</span>
` }
</label>
`)
},
create({ name, isGrouped, label, onChangeCB }) {
this.$el = this.template(name, isGrouped, label)
this.onChangeCB = onChangeCB
return this
},
render($container) {
$container.appendChild(this.$el)
this.$el.addEventListener('change', e => this.onChange(e))
return this
},
destroy() {
this.$el.remove()
return this
},
onChange(e) {
this.onChangeCB.call(this, e.target.checked)
}
}))
define('Game.Objects.Radio', ['Game.Atoms.Radio'], RadioAtom => _ => ({
create(options) {
return RadioAtom().create(Object.assign({}, options, {
onChangeCB: options.satisfactionCB
}))
}
}))
define('Game.Objects.RadioQuestion', [
'$',
'Game.Atoms.Radio',
'Game.Atoms.Button'
], (
$,
RadioAtom,
ButtonAtom
) => _ => ({
CLASS_NAME: 'radio-question-object',
template(label) {
return $(`
<form class="${ this.CLASS_NAME }">
<span class="${ this.CLASS_NAME }__title">
${ label }
</span>
<div
data-options
class="${ this.CLASS_NAME }__options">
</div>
</form>
`)
},
create({ data, withButton, satisfactionCB }) {
this.UID = 'rq-' + Date.now()
this.state = {
selected: null
}
this.$el = this.template(data.question)
this.data = data
this.withButton = withButton
this.satisfactionCB = satisfactionCB
return this
},
render($container) {
$container.appendChild(this.$el)
var self = this
this.data.options.forEach((option, index) => {
RadioAtom()
.create({
name: this.UID,
isGrouped: true,
label: option,
onChangeCB(isChecked) {
if (self.withButton) {
self.state.selected = index
} else if (self.data.answer === index) {
self.satisfactionCB.call(self)
}
}
})
.render(this.$el.querySelector('[data-options]'))
})
if (this.withButton) {
ButtonAtom()
.create({
onClickCB() {
if (self.state.selected === self.data.answer) {
self.satisfactionCB.call(self)
}
}
})
.render(this.$el)
}
return this
},
destroy() {
this.$el.remove()
return this
}
}))
define('Game.Objects.CheckboxQuestion', [
'$',
'Game.Atoms.Checkbox',
'Game.Atoms.Button'
], (
$,
CheckboxAtom,
ButtonAtom
) => _ => ({
CLASS_NAME: 'checkbox-question-object',
template(label) {
return $(`
<form class="${ this.CLASS_NAME }">
<span class="${ this.CLASS_NAME }__title">
${ label }
</span>
<div
data-options
class="${ this.CLASS_NAME }__options">
</div>
</form>
`)
},
create({ data, withButton, satisfactionCB }) {
this.UID = 'cq-' + Date.now()
this.state = {
options: data.options.map(o => 0)
}
this.$el = this.template(data.question)
this.data = data
this.withButton = withButton
this.satisfactionCB = satisfactionCB
return this
},
render($container) {
$container.appendChild(this.$el)
var self = this
this.data.options.forEach((option, index) => {
CheckboxAtom()
.create({
isGrouped: true,
label: option,
onChangeCB(isChecked) {
self.state.options[index] = +isChecked
if (!self.withButton) {
var flag = self.state.options.join('')
if (flag === self.data.answer) {
self.satisfactionCB.call(self)
}
}
}
})
.render(this.$el.querySelector('[data-options]'))
})
if (this.withButton) {
ButtonAtom()
.create({
onClickCB() {
var flag = self.state.options.join('')
if (flag === self.data.answer) {
self.satisfactionCB.call(self)
}
}
})
.render(this.$el)
}
return this
},
destroy() {
this.$el.remove()
return this
}
}))
define('Game.Objects.FallingLayer', ['$'], $ => _ => ({
CLASS_NAME: 'falling-layer-object',
template() {
return $(`
<div class="${ this.CLASS_NAME }"></div>
`)
},
create({ speed = 5, onEnd }) {
this.speed = speed
this.onEnd = onEnd
return this
},
render($parent) {
this.destroyed = false
this.noiseX = Math.random()
this.$el = this.template()
$parent.appendChild(this.$el)
requestAnimationFrame(_ => {
var s = this.getInitialSizes($parent)
var top = -s.height
var left = Math.random() * (s.parentWidth - s.width)
this.$el.style.cssText += `;
top: ${ top }px;
left: ${ left }px;
`
this.fall(Object.assign({}, s, { top, left }))
})
return this
},
destroy() {
this.$el.remove()
this.destroyed = true
return this
},
getInitialSizes($parent) {
return {
top: this.$el.offsetTop,
left: this.$el.offsetLeft,
width: this.$el.offsetWidth,
height: this.$el.offsetHeight,
parentWidth: $parent.offsetWidth,
parentHeight: $parent.offsetHeight
}
},
fall({
top,
left,
width,
height,
parentWidth,
parentHeight
}) {
requestAnimationFrame(_ => {
if (this.destroyed) {
return false
}
var incTop = 0
if (top + height < parentHeight) {
incTop = this.speed / 10
}
var incLeft = noise.simplex2(
this.noiseX / parentHeight,
top / parentHeight * this.speed
) * this.speed / 5
if (
left <= 0 && incLeft < 0 ||
left + width + incLeft >= parentWidth
) {
incLeft *= -1
}
if (incTop > 0) {
var newTop = top + incTop
var newLeft = left + incLeft
this.$el.style.cssText += `;
top: ${ newTop }px;
left: ${ newLeft }px;
`
this.fall({
top: newTop,
left: newLeft,
width,
height,
parentWidth,
parentHeight
})
} else {
this.onEnd()
}
})
}
}))
define('Game.Vocabulary.Texts', [
'hello',
'world'
])
define('Game.Vocabulary.CheckboxTitles', [
'Check this',
'Check',
''
])
define('Game.Vocabulary.NegativeCheckboxTitles', [
'Uncheck this',
'Uncheck',
''
])
define('Game.Vocabulary.RadioTitles', [
'Select this',
'Select',
''
])
define('Game.Vocabulary.ButtonTitles', [
'Click me',
'Click this',
'Click!'
])
define('Game.Vocabulary.TextQuestions', [{
question: 'The opposite of "yes"',
answer: 'no'
}, {
question: '2 + 2 = ?',
answer: '4'
}])
define('Game.Vocabulary.RadioQuestions', [{
question: 'Here is a question',
answer: 2,
options: [
'Really?',
'Oh, thanks!',
'No, it\'s not.',
'Sweet.'
]
}, {
question: 'Which is a fruit?',
answer: 1,
options: [
'A door',
'A banana',
'A computer',
'A cat'
]
}])
define('Game.Vocabulary.CheckboxQuestions', [{
question: 'A banana is',
answer: '0110',
options: [
'A vehicle',
'A fruit',
'Yellow or green',
'A person'
]
}, {
question: 'Which are correct?',
answer: '101',
options: [
'2^6 = 64',
'15 - 4 = 19',
'2 * 2 = 4'
]
}])
define('Game.Level', [
'Game.Vocabulary.Texts',
'Game.Vocabulary.CheckboxTitles',
'Game.Vocabulary.NegativeCheckboxTitles',
'Game.Vocabulary.RadioTitles',
'Game.Vocabulary.ButtonTitles',
'Game.Vocabulary.TextQuestions',
'Game.Vocabulary.RadioQuestions',
'Game.Vocabulary.CheckboxQuestions',
'Game.Objects.FallingLayer',
'Game.Objects.Text',
'Game.Objects.Checkbox',
'Game.Objects.Radio',
'Game.Objects.Button',
'Game.Objects.RadioQuestion',
'Game.Objects.CheckboxQuestion'
], (
VocTexts,
VocCheckboxTitles,
VocNegativeCheckboxTitles,
VocRadioTitles,
VocButtonTitles,
VocTextQuestions,
VocRadioQuestions,
VocCheckboxQuestions,
FallingLayerObject,
TextObject,
CheckboxObject,
RadioObject,
ButtonObject,
RadioQuestionObject,
CheckboxQuestionObject
) => _ => ({
setup({
$screen,
totalLevelCount,
difficulty,
onScore,
onDie,
onComplete
}) {
this.$screen = $screen
this.totalLevelCount = totalLevelCount
this.difficulty = difficulty
this.onScore = onScore
this.onDie = onDie
this.onComplete = onComplete
var _log2 = Math.log2(difficulty + 1)
this.objectCount = difficulty * 4
this.satisfiedCount = 0
this.launchedCount = 0
this.pointsPerObject = (250 * _log2) / this.objectCount
this.waitBeforeNextObject = 5000 / difficulty
this.noPointsAfter = 10000 / _log2
this.fullPointsBefore = 4000 / _log2
this.fallSpeed = 2 + _log2
this.ObjectTypes = {
Text: 0,
Checkbox: 1,
NegativeCheckbox: 2,
Radio: 3,
Button: 4,
TextQuestion: 5,
CheckboxQuestion: 6,
RadioQuestion: 7
}
return this
},
_getRandom(source) {
var slice
if (source.length < this.totalLevelCount) {
slice = source
} else {
var chunk = Math.max(Math.floor(source.length / this.totalLevelCount), 1)
var lowerBound = chunk * (this.difficulty - 1)
var upperBound = chunk * this.difficulty
slice = source.slice(lowerBound, upperBound)
}
return slice[Math.floor(Math.random() * slice.length)]
},
launchNewObject() {
var objectTypeCount = Object.keys(this.ObjectTypes).length
var RandomObject
var objectOptions
switch (Math.floor(Math.random() * objectTypeCount)) {
case this.ObjectTypes.Text:
RandomObject = TextObject
objectOptions = {
whatToType: this._getRandom(VocTexts)
}
break
case this.ObjectTypes.Checkbox:
RandomObject = CheckboxObject
objectOptions = {
label: this._getRandom(VocCheckboxTitles)
}
break
case this.ObjectTypes.NegativeCheckbox:
RandomObject = CheckboxObject
objectOptions = {
isNegative: true,
isChecked: true,
label: this._getRandom(VocNegativeCheckboxTitles)
}
break
case this.ObjectTypes.Radio:
RandomObject = RadioObject
objectOptions = {
label: this._getRandom(VocRadioTitles)
}
break
case this.ObjectTypes.Button:
RandomObject = ButtonObject
objectOptions = {
label: this._getRandom(VocButtonTitles)
}
break
case this.ObjectTypes.TextQuestion:
RandomObject = TextObject
var data = this._getRandom(VocTextQuestions)
objectOptions = {
whatToType: data.answer,
question: data.question
}
break
case this.ObjectTypes.CheckboxQuestion:
RandomObject = CheckboxQuestionObject
objectOptions = {
data: this._getRandom(VocCheckboxQuestions)
}
break
case this.ObjectTypes.RadioQuestion:
RandomObject = RadioQuestionObject
objectOptions = {
data: this._getRandom(VocRadioQuestions)
}
break
}
var launchTime = Date.now()
var $fallingLayer = FallingLayerObject()
.create({
speed: this.fallSpeed,
onEnd: _ => this.onDie()
})
.render(this.$screen)
var self = this
RandomObject()
.create(Object.assign({}, objectOptions, {
satisfactionCB() {
this.destroy()
$fallingLayer.destroy()
var startTime = launchTime + self.fullPointsBefore
var diff = Math.max(Date.now() - startTime, 0)
var ratio = Math.max(self.noPointsAfter / diff, 1)
var divider = Math.sin((Math.PI / 2) / ratio)
var score = self.pointsPerObject * (1 - divider)
console.log(diff, score)
self.onScore(score)
if (++self.satisfiedCount === self.objectCount) {
self.onComplete()
}
}
}))
.render($fallingLayer.$el)
if (++this.launchedCount < this.objectCount) {
setTimeout(
this.launchNewObject.bind(this),
this.waitBeforeNextObject
)
}
},
start() {
this.launchNewObject()
}
}))
define('Game', [
'$',
'State',
'Game.Globals.SeedNoise',
'Game.Level'
], (
$,
State,
SeedNoise,
Level
) => {
SeedNoise()
var state = State({
score: 0,
level: 1,
lives: 3
}, changed => {
renderHUD()
})
var $screen = $('#game-screen')
var $lives = $('#game-lives')
var $score = $('#game-score')
function renderHUD() {
$lives.innerHTML = new Array(state.lives + 1).join(`
<span class="game__lives-heart">♥︎</span>
`)
$score.innerText = `Score: ${ parseInt(state.score, 10) }`
}
function startLevel(lvl) {
var $levelTitle = $(`<h2>Level ${ lvl + 1 }</h2>`)
$screen.appendChild($levelTitle)
setTimeout(_ => {
$levelTitle.remove()
levels[lvl].start()
}, 1500)
}
function endGame() {
$screen.appendChild($(`<h1>The end.</h1>`))
}
function startGame() {
startLevel(0)
renderHUD()
}
startGame()
var levelCount = 10
var levels = new Array(levelCount)
.join(' ')
.split(' ')
.map((e, lvl) => Level().setup({
$screen,
totalLevelCount: levelCount,
difficulty: lvl + 1,
onScore(score) {
state.update({
score: state.score + score
})
},
onDie() {
state.update({
lives: state.lives - 1
})
},
onComplete() {
state.update({
level: state.level + 1
})
setTimeout(_ => {
if (lvl + 1 < levels.length) {
startLevel(lvl + 1)
} else {
endGame()
}
}, 1500)
}
}))
})
<script src="http://codepen.io/pavlovsk/pen/bBqeBB"></script>
<script src="https://cdn.rawgit.com/josephg/noisejs/master/perlin.js"></script>
* {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font: 100 normal 16px/1.5 helvetica, sans-serif;
background: #ddd;
color: #1d1d1d;
}
.game {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 500px;
height: 90%;
max-height: 720px;
padding: 0 1.5em;
background: #fff;
border-radius: 3px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, .1);
&__title {
text-align: center;
margin: 0;
padding: .5em;
}
&__screen {
position: relative;
height: 100%;
overflow: hidden;
background: #f0f0f0;
box-shadow: inset 2px 2px 10px rgba(0, 0, 0, .1);
}
&__hud {
display: flex;
justify-content: space-between;
align-items: center;
height: 80px;
}
&__lives-heart {
color: red;
font-size: 1.5em;
}
&__score {
font-family: 'Libre Baskerville', serif;
}
}
.text-object,
.checkbox-object:not(.checkbox-object--grouped),
.radio-object:not(.radio-object--grouped),
.button-object,
.radio-question-object,
.checkbox-question-object {
padding: .5em;
font-size: .8em;
border-radius: 3px;
}
.text-object {
display: inline-block;
width: 120px;
background: #3C3B5C;
color: #fff;
border-bottom: 1px solid hsla(240, 42%, 20%, 1);
&__title {
display: block;
text-align: center;
}
&__input {
width: 100%;
padding: .25em;
font-size: 15px;
border: 1px solid #ccc;
border-radius: 1px;
transition: all .3s;
}
&--invalid &__input {
border: 1px solid #f55;
outline-color: #f55;
}
}
.checkbox-object,
.radio-object {
display: inline-block;
&__check {
margin: 0;
}
}
.checkbox-object:not(.checkbox-object--grouped) {
background: #FFB563;
border-bottom: 1px solid hsla(30, 100%, 56%, 1);
}
.radio-object:not(.radio-object--grouped) {
background: #7B3C59;
color: #fff;
border-bottom: 1px solid hsla(337, 50%, 20%, 1);
}
.radio-question-object,
.checkbox-question-object {
display: inline-block;
&__title {
display: inline-block;
padding: 0 .25em;
}
}
.radio-question-object {
color: #fff;
background: #7B3C59;
border-bottom: 1px solid hsla(337, 50%, 20%, 1);
.radio-object--grouped {
display: block;
}
}
.checkbox-question-object {
background: #FFB563;
border-bottom: 1px solid hsla(30, 100%, 56%, 1);
.checkbox-object--grouped {
display: block;
}
}
.button-object {
background: #D53939;
color: #fff;
border: none;
border-bottom: 1px solid hsla(0, 80%, 25%, 1);
line-height: 1.5
}
.falling-layer-object {
display: inline-block;
position: absolute;
}
<link href="https://fonts.googleapis.com/css?family=Libre+Baskerville:700" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment