Last active
September 3, 2015 17:12
-
-
Save zeffii/57260515b9afb14eee99 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### | |
code by Dealga McArlde, 2015. | |
Trademarks and copyrights held by the respective owners. | |
Likeness reproduced merely out of curiosity without commercial intent. | |
- Simon is an SVG element, generated on the fly using d3.js | |
- SVG interaction is handled by JQuery | |
- SVG transitions are handled by d3.js / JQuery | |
- I've disabled some of the SVG filters to reduce resource consumption. | |
- I had lofty plans for a full replica of the 1978 simon game, but it's too | |
different from the challenge's checklist, some UI is not hooked up but the user | |
is notified. | |
- TADAA: | |
2dogSound_tadaa1_3s_2013jan31_CC-BY-30-US.wav by rdholder | |
shared under the Creative Commons Attribution license. | |
- FAIL: | |
AdamWeeden CC-BY-30 Attribution License.. | |
https://freesound.org/people/AdamWeeden/sounds/157218/ | |
- I'm starting to dislike the game. | |
[X] done | |
[-] some code in place | |
[ ] no code. | |
milestones | |
[x] sounds when pressed, fail, win | |
[x] UI likeness (Strict, Start, Restart, On/Off, pads, counter) | |
[x] present with random sequences of button presses, up to 20 | |
[x] light up & trigger.play for each step needed. | |
[x] progression to an extra step if user reproduces steps correctly | |
[x] notify on incorrect step, play sequence again to allow user to try again | |
[x] ---- strict mode, notifies on failure and starts a new sequence from 1 | |
[x] see how many steps current set has | |
[x] option to restart from step 1 | |
[x] 5,9.13 steps increase speed | |
[x] step 20 notifies victory, starts over with a new sequence. | |
### | |
GAME = 1 | |
SKILL = 3 | |
POWER = 0 | |
STRICT = 0 | |
SEQUENCE = [] | |
STEPS = 20 | |
USER_SEQUENCE = [] | |
CORRECT_STEPS = 0 | |
STEPS_PLAYED = 0 | |
timerVarPlayer = {} | |
position = 0 | |
interim_step = 0 | |
STATE = ['PLAYING', 'LISTENING', 'OFF'][0] | |
spacer = 15 | |
baserUrl = "https://s3.amazonaws.com/freecodecamp/simonSound" | |
s1 = new Audio(baserUrl + "1.mp3") | |
s2 = new Audio(baserUrl + "2.mp3") | |
s3 = new Audio(baserUrl + "3.mp3") | |
s4 = new Audio(baserUrl + "4.mp3") | |
TADAA = new Audio("http://freesound.org/data/previews/177/177120_3287045-lq.mp3") | |
FAIL = new Audio("http://freesound.org/data/previews/157/157218_1670580-lq.mp3") | |
# ------------------ Utility Function --------------------------------------- | |
load_new_sound = (idx) -> | |
f = new Audio(baserUrl + (idx) + ".mp3") | |
f.play() | |
# is f garbage collected after load_new_sound function completes? end of scope | |
# appears to suggest it is.. :) | |
# http://stackoverflow.com/questions/742623/deleting-objects-in-javascript | |
sound_player = (pad) -> | |
load_new_sound(1) if pad in ['GREEN', 0] | |
load_new_sound(2) if pad in ['YELLOW', 1] | |
load_new_sound(3) if pad in ['BLUE', 2] | |
load_new_sound(4) if pad in ['RED', 3] | |
play_sound_delayed = (sound, delay) -> | |
setTimeout (-> | |
sound.play() | |
return | |
), delay | |
get_speed = (player_step) -> | |
return 1000 if player_step in [0..4] | |
return 800 if player_step in [5..8] | |
return 600 if player_step in [9..12] | |
return 400 if player_step >= 13 | |
getSize = () -> | |
d = document.documentElement | |
[d.clientWidth, d.clientHeight] | |
get_id_from_index = (idx) -> | |
{0: 'GREEN', 1 :'YELLOW', 2 :'BLUE', 3 :'RED'}[idx] | |
get_index_from_id = (id) -> | |
{'GREEN': 0, 'YELLOW': 1, 'BLUE': 2, 'RED': 3}[id] | |
translate = (w, h) -> | |
"translate(" + [w, h] + ")" | |
make_centered_rect = (w, h) -> | |
'M' + [-w/2, h, w/2, h, w/2, 0, -w/2, 0] | |
generate_rnd_seq = (sizer) -> | |
Math.round((Math.random() * 3)) for [0...sizer] | |
make_semi_circle = (R, segments, frustum, sign) -> | |
theta = Math.PI * 2 / segments | |
limits = (i) -> | |
if sign == 'LESS' | |
return ((theta*i < frustum) or (theta*i > (Math.PI*2)-frustum)) | |
if sign == 'MORE' | |
return (theta*i > frustum) and (theta*i < (Math.PI*2)-frustum) | |
m = ([Math.sin(theta*i)*R, Math.cos(theta*i)*R] for i in [0..segments] when limits(i)) | |
'M' + m + 'z' | |
make_arc = (start, end) -> | |
d3.svg.arc() | |
.outerRadius(270) | |
.innerRadius(140) | |
.cornerRadius(20) | |
.padAngle(0.1) | |
.startAngle(start) | |
.endAngle(end) | |
# ----------------------------------SVG construction ----------------------------------- | |
svg = d3.select 'svg' | |
g = svg.append 'g' | |
.classed 'bg_group', true | |
draw_device = (w,h) -> | |
g = d3.select '.bg_group' | |
g.attr transform: "translate(" + [w / 2, h / 2] + ")" | |
g.append 'circle' | |
.classed 'circle_shape_bg', true | |
.attr r: 290 | |
g.append 'circle' | |
.classed 'circle_shape_fg', true | |
.attr r: 118 | |
g.append 'path' | |
.classed 'dome', true | |
.attr d: make_arc(0, Math.PI / 2), id: 'RED' | |
g.append 'path' | |
.classed 'dome', true | |
.attr d: make_arc(Math.PI / 2, Math.PI), id: 'BLUE' | |
g.append 'path' | |
.classed 'dome', true | |
.attr d: make_arc(Math.PI, Math.PI * 1.5), id: 'YELLOW' | |
g.append 'path' | |
.classed 'dome', true | |
.attr d: make_arc(Math.PI * 1.5, Math.PI * 2), id: 'GREEN' | |
g.append 'path' | |
.classed 'label top', true | |
.attr d: make_semi_circle(112, 90, 1.9, 'MORE') | |
g.append 'path' | |
.classed 'label bottom', true | |
.attr d: make_semi_circle(112, 90, 1.89, 'LESS') | |
g.append 'text' | |
.classed 'simon_typography unselectable', true | |
.text 'SIMON' | |
.attr transform: 'translate(0, -48),scale(1,0.5)' | |
signwide = 49 | |
signage = {w: signwide, h: 30} | |
SIGN = g.append 'g' | |
.attr transform: translate(-signage.w / 2, -107) | |
SIGN.append 'rect' | |
.classed 'tat', true | |
.attr | |
height: signage.h | |
width: signage.w | |
rx: 3, ry: 3 | |
SIGN.append 'text' | |
.classed 'stepshow unselectable', true | |
.attr transform: translate(signwide/2,23) | |
.text '' | |
# ----------------- UI ----- ( inside circle plate ) ----------- | |
button_spread = 60 | |
button_height = 53 | |
make_button = (className, idName, gTranslate, text_content) -> | |
G = g.append 'g' | |
.attr transform: translate(gTranslate, button_height) | |
G.append 'circle' | |
.classed className, true | |
.attr r: 13, id: idName | |
G.append 'text' | |
.classed 'ui_text unselectable', true | |
.text text_content | |
.attr transform: translate(0,-20) | |
make_button('button yellow', 'b1', -button_spread, 'STRICT') | |
make_button('button red', 'b2', 0, 'START') | |
make_button('button yellow', 'b3', button_spread, 'RESTART') | |
g.append 'circle' | |
.classed 'strict_notifier', true | |
.attr r: 6, transform: translate(-86, 44) | |
make_knob = (width, x, y, idName) -> | |
if idName in ['k1', 'k2'] | |
[col1, col2, col3, col4] = ["#aaccff","#88aaff",'#222222','#77f7ff'] | |
else | |
[col1, col2, col3, col4] = ["#ff7755","#ffaa88",'#332222','#fff7e7'] | |
knob_height = 20 | |
gr = g.append 'g' | |
.attr transform: translate(x, y), id: idName | |
.classed 'knob', true | |
gr.append 'path' | |
.attr d: make_centered_rect(width, knob_height) | |
.style fill: col1, filter: "url(#f4)" | |
gr.append 'path' | |
.attr d: make_centered_rect(14, knob_height) | |
.style fill: col2 | |
gr.append 'path' | |
.attr d: 'M' + [0, knob_height, 0, 0] | |
.style stroke: col3, 'stroke-width': 1 | |
gr.append 'path' | |
.attr d: 'M' + [2, knob_height, 2, 0] | |
.style stroke: col4, 'stroke-width': 1 | |
make_knob(40, -70, -4, 'k1') | |
make_knob(70, 28 + (2*spacer), -4, 'k2') # (30)..(30 + (3*15)) | |
make_knob(40, -7, 75, 'k3') | |
# text for knobs | |
make_text = (content, pos) -> | |
g.append 'text' | |
.classed 'ui_text unselectable', true | |
.text content | |
.attr transform: translate(pos.x, pos.y) | |
make_text('ON', {x:40, y: 90}) | |
make_text('OFF', {x:-43, y: 90}) | |
make_text('GAME', {x:-52, y: -21}) | |
num_start = -69 | |
make_text('1', {x:num_start, y: -9}) | |
make_text('2', {x:num_start+spacer, y: -9}) | |
make_text('3', {x:num_start+(spacer*2), y: -9}) | |
num_start = 29 | |
make_text('1', {x:num_start, y: -9}) | |
make_text('2', {x:num_start+spacer, y: -9}) | |
make_text('3', {x:num_start+(spacer*2), y: -9}) | |
make_text('4', {x:num_start+(spacer*3), y: -9}) | |
make_text('SKILL LEVEL', {x:num_start+26, y: -21}) | |
make_infotak = (idName, ypos, textClass, sentencelength) -> | |
uu = 5 | |
CLO = g.append 'g' | |
.attr transform: translate(-310, ypos), id: idName | |
.attr opacity: 0.0 | |
CLO.append 'rect' | |
.attr width: sentencelength, height: 30, y: -15, opacity: 0.2, rx: 12, ry: 12 | |
CLO.append 'circle' | |
.classed 'text_closer', true | |
.attr r: '20px' | |
.style fill: '#353535', stroke: '#000', 'stroke-width': '1px' | |
CLO.append 'path' | |
.attr d: 'M' + [-uu,-uu,uu,uu] + 'M' + [-uu,uu,uu,-uu] | |
.style stroke: '#eee', 'stroke-width': '2px' | |
CLO.append 'text' | |
.classed 'information', true | |
.attr transform: translate(29, 5), id: textClass | |
.text '' | |
info_height = -225 | |
make_infotak('to_close', info_height, 'regret', 530) | |
# --------------- animation stuff -------------- | |
on_off_element = (ELEM, HIGH, LOW) -> | |
ELEM.transition(50).style('fill', HIGH).transition(50).style('fill', LOW) | |
switcher = (ELEM, FINAL) -> | |
ELEM.transition(100).style('fill', FINAL) | |
animate_element = (ID) -> | |
# [ ] these should all be hex, for consistency | |
D3E = d3.select('#'+ID) | |
if ID == "RED" | |
on_off_element(D3E, 'hsl(5, 93, 60)', 'hsl(0, 68, 60)') | |
if ID == "GREEN" | |
on_off_element(D3E, 'hsl(126, 69, 68)', 'hsl(120, 39, 62)') | |
if ID == "YELLOW" | |
on_off_element(D3E, '#FFFF3C', '#E2E25A') | |
if ID == "BLUE" | |
on_off_element(D3E, '#58B5FF', 'hsl(207, 100, 42)') | |
# ------------- TIMER STUFF ----------------------------- | |
# ----PLAYING | |
removeTimer = (show_txt) -> | |
position = 0 | |
$('.stepshow').text(show_txt) | |
window.clearInterval timerVarPlayer | |
myTimer = (limit) -> | |
# limit is the current number of succesful steps reproduced by user. | |
if (position >= limit+1) or (position >= STEPS) or (POWER == 0) | |
STATE = if (POWER == 0) then 'OFF' else 'LISTENING' | |
removeTimer('--') | |
if STATE == 'LISTENING' | |
interim_step = 0 | |
STEPS_PLAYED = limit+1 | |
return | |
if position < SEQUENCE.length | |
sound_idx = SEQUENCE[position] | |
ID = get_id_from_index(sound_idx) | |
animate_element(ID) | |
$('.stepshow').text(position+1) | |
sound_player(sound_idx) | |
position += 1 | |
play_next_set_delayed = (reset, position, delay) -> | |
setTimeout (-> | |
if reset | |
SEQUENCE = generate_rnd_seq(STEPS) | |
start_playing(position) | |
return | |
), delay | |
# ----------------- GAME ENGINE ------------------------- | |
start_playing = (num_steps_to_play) -> | |
console.log 'startin sequence play!' | |
# always remove any playing timeVar first! | |
USER_SEQUENCE = [] | |
removeTimer('--') | |
num_steps_to_play = num_steps_to_play | |
speed = get_speed(num_steps_to_play) | |
timerVarPlayer = setInterval((-> | |
myTimer(num_steps_to_play) | |
return | |
), speed) | |
# -------------------------UI (click handling) --------------------------- | |
$(window).ready( () -> | |
[dw, dh] = getSize() | |
draw_device(dw, dh) | |
$('.dome').click( () -> | |
if POWER == 1 | |
if STATE == 'LISTENING' | |
USER_SEQUENCE.push( get_index_from_id(@.id) ) | |
console.log(USER_SEQUENCE) | |
console.log(SEQUENCE) | |
animate_element(@.id) | |
sound_player(@.id) | |
if STATE == 'LISTENING' | |
if USER_SEQUENCE.length <= STEPS_PLAYED | |
a = SEQUENCE[interim_step] | |
b = USER_SEQUENCE[interim_step] | |
if a == b | |
interim_step += 1 | |
$('.stepshow').text(interim_step) | |
if USER_SEQUENCE.length == SEQUENCE.length | |
play_sound_delayed(TADAA, 1200) | |
play_next_set_delayed(true, 0, 2000) | |
return | |
else | |
play_sound_delayed(FAIL, 600) | |
if STRICT | |
play_next_set_delayed(true, 0, 2000) | |
else | |
play_next_set_delayed(false, STEPS_PLAYED-1, 2000) | |
if (interim_step) == (STEPS_PLAYED) | |
STATE == 'PLAYING' | |
start_playing(STEPS_PLAYED) | |
) | |
$('.knob').click( () -> | |
# POWER KNOB | |
if @.id == 'k3' | |
POWER = if POWER == 0 then 1 else 0 | |
xpos = if POWER == 0 then -7 else 8 | |
$(@).attr('transform', translate(xpos, 75)) | |
marks = if POWER == 0 then '' else '--' | |
$('.stepshow').text(marks) | |
if POWER == 0 | |
removeTimer('') | |
ELEM = d3.select('.strict_notifier') | |
STRICT = 0 | |
switcher(ELEM, '#A22') | |
USER_SEQUENCE = [] | |
# $(this).attr('transform', translate(-70 + ((GAME-1) * spacer), -4)) | |
# $(this).attr('transform', translate(28 + ((SKILL-1) * spacer), -4)) | |
if @.id in ['k1', 'k2'] | |
msg = 'game types and skill levels are locked..for your protection.' | |
$('text#regret').text(msg) | |
$('#to_close').animate({'opacity': 1.0}, 300) | |
) | |
$('.button').click( () -> | |
# STRICT | |
if @.id == 'b1' | |
if POWER == 1 | |
STRICT = if STRICT == 0 then 1 else 0 | |
colour = if STRICT == 0 then '#a22' else '#F7A' | |
ELEM = d3.select('.strict_notifier') | |
switcher(ELEM, colour) | |
# START | |
if @.id == 'b2' | |
if POWER == 1 | |
SEQUENCE = generate_rnd_seq(STEPS) | |
start_playing(0) | |
# RESTART | reuses exhisting seq or makes new one if empty | |
if @.id == 'b3' | |
if SEQUENCE.length == 0 | |
SEQUENCE = generate_rnd_seq(STEPS) | |
start_playing(0) | |
) | |
$('g#to_close').click( () -> | |
$(@).animate({'opacity': 0.0}, 300, () -> $('text#regret').text('') ) | |
) | |
) | |
$(window).resize( () -> | |
[dw, dh] = getSize() | |
g = d3.select '.bg_group' | |
g.attr transform: translate(dw / 2, dh / 2) | |
) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href='https://fonts.googleapis.com/css?family=Squada+One' rel='stylesheet' type='text/css'> | |
<svg class='simon_svg'> | |
<defs> | |
<radialGradient id="grad1" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"> | |
<stop offset="0%" style="stop-color:rgb(125,125,125); | |
stop-opacity:1" /> | |
<stop offset="100%" style="stop-color:rgb(88,90,90);stop-opacity:1" /> | |
</radialGradient> | |
<filter id="f3" x="-20%" y="-20%" width="140%" height="140%"> | |
<feOffset result="offOut" in="SourceAlpha" dx="0.08" dy="0.08" /> | |
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="6" /> | |
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" /> | |
</filter> | |
<filter id="f4" x="-10%" y="-10%" width="50px" height="50px"> | |
<feOffset result="offOut" in="SourceAlpha" dx="0.039" dy="0.039" /> | |
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="1" /> | |
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" /> | |
</filter> | |
<filter id="MyFilter" filterUnits="userSpaceOnUse" x="-60%" y="-60%" width="160%" height="160%"> | |
<feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/> | |
<feOffset in="blur" dx="1.2" dy="1.2" result="offsetBlur"/> | |
<feSpecularLighting in="blur" surfaceScale="-.5" specularConstant=".75" | |
specularExponent="50" lighting-color="#bbbbbb" | |
result="specOut"> | |
<fePointLight x="-5000" y="-10000" z="20000"/> | |
</feSpecularLighting> | |
<feComposite in="specOut" in2="SourceAlpha" operator="in" result="specOut"/> | |
<feComposite in="SourceGraphic" in2="specOut" operator="arithmetic" | |
k1="0" k2="1" k3="1" k4="0" result="litPaint"/> | |
<feMerge> | |
<feMergeNode in="offsetBlur"/> | |
<feMergeNode in="litPaint"/> | |
</feMerge> | |
</filter> | |
</defs> | |
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$label-color: #505050 | |
$simon-color: #595959 | |
$simon-white: #fafafa | |
.simon_typography | |
font-family: 'Squada One' | |
font-size: 72px | |
text-anchor: middle | |
fill: $simon-white | |
.information | |
font-family: 'Squada One' | |
font-size: 22px | |
fill: $simon-white | |
body | |
background: url("http://i.stack.imgur.com/7YKUD.jpg"), repeat | |
.simon_svg | |
width: 100vw | |
height: 100vh | |
.bg_group | |
width: 50px | |
height: 50px | |
.circle_shape_bg | |
filter: url(#f3) | |
fill: url(#grad1) | |
stroke: none | |
.circle_shape_fg | |
fill: #efefef | |
stroke: none | |
filter: url(#f3) | |
.dome | |
// filter: url(#MyFilter) | |
stroke: #345 | |
#RED | |
fill: hsl(0, 68, 60) | |
#GREEN | |
fill: hsl(120, 39, 62) | |
#YELLOW | |
fill: #E2E25A | |
#BLUE | |
fill: hsl(207, 100, 42) | |
.label | |
stroke: $label-color | |
.bottom | |
fill: none | |
stroke-width: 1.8px | |
.top | |
fill: $label-color | |
.tat | |
fill: #772222 | |
stroke: $simon-white | |
stroke-width: 2px | |
.button | |
stroke: #636363 | |
stroke-width: 5px | |
.yellow | |
fill: #ff6 | |
.red | |
fill: #ff8e8e | |
.ui_text | |
font-size: 10px | |
font-family: sans-serif | |
text-anchor: middle | |
.unselectable | |
-webkit-touch-callout: none | |
-webkit-user-select: none | |
-khtml-user-select: none | |
-moz-user-select: none | |
-ms-user-select: none | |
user-select: none | |
.stepshow | |
font-family: monospace | |
font-size: 24px | |
fill: #FF5555 | |
letter-spacing: 4px | |
text-anchor: middle | |
.strict_notifier | |
fill: #a22 | |
stroke: #a7a | |
stroke-width: 2px |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment