Skip to content

Instantly share code, notes, and snippets.

@zeffii
Last active September 3, 2015 17:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zeffii/57260515b9afb14eee99 to your computer and use it in GitHub Desktop.
Save zeffii/57260515b9afb14eee99 to your computer and use it in GitHub Desktop.
###
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)
)
<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>
$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