Last active
August 29, 2015 13:57
-
-
Save br0xen/9507533 to your computer and use it in GitHub Desktop.
devict.org Code Challenge - Bowling Score
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
/* bowling.js - Track a bowling score | |
* devict.org coding challenge. | |
* To see it in action go to: | |
* http://dev.bullercodeworks.com/bowling.html | |
*/ | |
// To keep bowling score, we must first write a DOM manipulation framework. | |
function B(els, attrs) { | |
// Turn 'this' into an array of passed in elements. | |
function B(els) { | |
if(typeof els === "string") { | |
els = this.brb.create(els); | |
} | |
for(var i = 0; i < els.length; i++) { | |
this[i] = els[i]; | |
} | |
this.length = els.length; | |
} | |
// Map a function to all elements in 'this' | |
B.prototype.map = function(callback) { | |
var results = [], i = 0; | |
for(;i<this.length;i++){ | |
results.push(callback.call(this,this[i],i)); | |
} | |
return results; | |
}; | |
// Foreach through all elements in 'this' | |
B.prototype.forEach = function(callback) { | |
this.map(callback); | |
return this; | |
}; | |
// Map a function to the first element in 'this' | |
B.prototype.mapOne = function(callback) { | |
var m = this.map(callback); | |
return m.length > 1 ? m : m[0]; | |
}; | |
// Update css for each element in 'this' | |
B.prototype.css = function(css_opt_var, css_opt_val) { | |
if(typeof css_opt_var !== "string") { | |
for(css_var in css_opt_var) { | |
this.forEach(function(el){el.style[css_var]=css_opt_var[css_var];}); | |
} | |
return this; | |
} else { | |
if(typeof css_opt_val !== "undefined") { | |
return this.forEach(function(el){el.style[css_opt_var]=css_opt_val;}); | |
} else { | |
return this.mapOne(function(el){return el.style[css_opt_var];}); | |
} | |
} | |
}; | |
// Update the innerText for each element in 'this' | |
B.prototype.text = function(text) { | |
if(typeof text !== "undefined") { | |
return this.forEach(function(el){el.innerText=text;}); | |
} else { | |
return this.mapOne(function(el){return el.innerText;}); | |
} | |
}; | |
// Add a class to each element in 'this' | |
B.prototype.addClass = function(classes) { | |
var className = ""; | |
if(typeof classes !== "string") { | |
for(var i=0;i<classes.length;i++) { | |
className+=" "+classes[i]; | |
} | |
} else { | |
className=" "+classes; | |
} | |
return this.forEach(function(el){el.className+=className;}); | |
}; | |
// Remove a class from each element in 'this' | |
B.prototype.removeClass = function(remove_class) { | |
return this.forEach(function(el){ | |
var cs = el.className.split(" "), i; | |
while((i=cs.iindexOf(remove_class))>-1){ | |
cs = cs.slice(0,i).concat(cs.slice(++i)); | |
} | |
el.className=cs.join(" "); | |
}); | |
}; | |
// Set an attribute for each element in 'this' | |
B.prototype.attr = function(attr,val){ | |
if(typeof val!=="undefined"){ | |
if(this[0].tagName=="INPUT" && attr.toUpperCase()=="VALUE") { | |
// If we're setting the 'VALUE' then it's actually .value | |
return this.forEach(function(el){ | |
el.value=val; | |
}); | |
} else { | |
// Otherwise use .setAttribute | |
return this.forEach(function(el){ | |
el.setAttribute(attr,val); | |
}); | |
} | |
} else { | |
// And clearing the value | |
if(this[0].tagName=="INPUT" && attr.toUpperCase()=="VALUE") { | |
return this.mapOne(function(el){ | |
return el.value; | |
}); | |
} else { | |
return this.mapOne(function(el){ | |
return el.getAttribute(attr); | |
}); | |
} | |
} | |
}; | |
// Actually set a value on each element (can be done with attr too.) | |
B.prototype.val = function(new_val) { | |
if(typeof new_val!=="undefined"){ | |
return this.forEach(function(el){ | |
el.value = new_val; | |
}); | |
} else { | |
// Just retrieve the value for the first element | |
return this.mapOne(function(el) { | |
return el.value; | |
}); | |
} | |
} | |
// Append an element to the DOM after each element in 'this' | |
B.prototype.append = function(els) { | |
this.forEach(function(parEl, i) { | |
els.forEach(function(childEl) { | |
if(i>0) { | |
childEl=childEl.cloneNode(true); | |
} | |
parEl.appendChild(childEl); | |
}); | |
}); | |
}; | |
// Prepend an element to the DOM before each element in 'this' | |
B.prototype.prepend = function(els) { | |
return this.forEach(function(parEl, i) { | |
for(var j = els.length-1; j>-1; j--) { | |
childEl=(i>0)?els[j].cloneNode(true):els[j]; | |
parEl.insertBefore(childEl, parEl.firstChild); | |
} | |
}); | |
}; | |
// Remove all elements in 'this' from the DOM | |
B.prototype.remove = function() { | |
return this.forEach(function(el){ | |
return el.parentNode.removeChild(el); | |
}); | |
}; | |
// Add an event listener to each element in 'this' | |
B.prototype.on = (function(){ | |
// Browser compatibility... | |
if(document.addEventListener) { | |
return function(evt,fn) { | |
return this.forEach(function(el){ | |
el.addEventListener(evt, fn, false); | |
}); | |
}; | |
} else if(document.attachEvent) { | |
return function(evt,fn) { | |
return this.forEach(function(el){ | |
el.attachEvent("on"+evt,fn); | |
}); | |
}; | |
} else { | |
return function(evt, fn) { | |
return this.forEach(function(el){ | |
el["on"+evt]=fn; | |
}); | |
}; | |
} | |
}()); | |
// Disable event listeners on elements in 'this' | |
B.prototype.off = (function(){ | |
// Browser compatibility... | |
if(document.removeEventListener) { | |
return function(evt, fn) { | |
return this.forEach(function(el) { | |
el.removeEventListener(evt, fn, false); | |
}); | |
}; | |
} else if(document.detachEvent) { | |
return function(evt, fn) { | |
return this.forEach(function(el) { | |
el.detachEvent("on"+evt, fn); | |
}); | |
}; | |
} else { | |
return function(evt, fn) { | |
return this.forEach(function(el){ | |
el["on"+evt]=null; | |
}); | |
}; | |
} | |
}()); | |
// The actual framework object, yay! | |
var brb = { | |
// Get an element | |
get: function(selector) { | |
var els; | |
if(typeof selector === "string") { | |
els = document.querySelectorAll(selector); | |
} else if(selector.length) { | |
els = selector; | |
} else { | |
els = [selector]; | |
} | |
return new B(els); | |
}, | |
// Create a new element | |
create: function(tagName, attrs) { | |
var el = new B([document.createElement(tagName)]); | |
// Set attributes on new element | |
if(attrs) { | |
if(attrs.className) { | |
// Classes | |
el.addClass(attrs.className); | |
delete attrs.classname; | |
} | |
if(attrs.text) { | |
// Text | |
el.text(attrs.text); | |
delete attrs.text; | |
} | |
for(var key in attrs) { | |
// All other Attributes | |
if(attrs.hasOwnProperty(key)) { | |
el.attr(key, attrs[key]); | |
} | |
} | |
} | |
return el; | |
} | |
}; | |
var match_tags = els.match(/<([^>\s\/]*)\s?\/?>/); | |
if(match_tags && match_tags.length > 0) { | |
// It's a 'create tag' command | |
return brb.create(match_tags[1], attrs); | |
} else { | |
// Just search for matches | |
return brb.get(els); | |
} | |
}; | |
// And there is our minimalist framework. Who needs jquery? | |
// Now start with the bowling scorer | |
function calculate_frame_score(frame_num) { | |
if(isNaN(frame_num) || frame_num < 1 || frame_num > 10) { | |
return 0; | |
} | |
var frame_score = 0; | |
var ball_1 = B('#frame_'+frame_num+'_ball_1').text(); | |
var strike_ball_1, strike_ball_2; | |
if(ball_1 == 'X') { | |
frame_score += 10; | |
// Strike, get the first ball of the next frame | |
if(frame_num == 9) { | |
// Frame 9 is Special for Strikes | |
strike_ball_1 = B('#frame_10_ball_1').text(); | |
strike_ball_2 = B('#frame_10_ball_2').text(); | |
} else if(frame_num == 10) { | |
// Frame 10 is Special for Strikes | |
strike_ball_1 = B('#frame_10_ball_2').text(); | |
strike_ball_2 = B('#frame_10_ball_3').text(); | |
} else { | |
// All other frames | |
strike_ball_1 = B('#frame_'+(frame_num+1)+'_ball_1').text(); | |
if(strike_ball_1 == 'X') { | |
strike_ball_2 = B('#frame_'+(frame_num+2)+'_ball_1').text(); | |
} else { | |
strike_ball_2 = B('#frame_'+(frame_num+1)+'_ball_2').text(); | |
} | |
} | |
if(strike_ball_1 == 'X') { | |
// Another Strike! | |
frame_score += 10; | |
} else { | |
if(!isNaN(parseInt(strike_ball_1))) { | |
frame_score += parseInt(strike_ball_1); | |
} | |
} | |
if(strike_ball_2 == 'X' || strike_ball_2 == '/') { | |
frame_score += 10; | |
} else { | |
if(!isNaN(parseInt(strike_ball_2))) { | |
frame_score += parseInt(strike_ball_2); | |
} | |
} | |
} else { | |
var ball_2 = B('#frame_'+frame_num+'_ball_2').text(); | |
if(ball_2 == '/') { | |
// Handle Spares | |
var spare_ball_1; | |
if(frame_num == 10) { | |
spare_ball_1 = B('#frame_10_ball_3').text(); | |
} else { | |
spare_ball_1 = B('#frame_'+(frame_num+1)+'_ball_1').text(); | |
} | |
frame_score += 10; | |
// Spare! | |
if(spare_ball_1 == 'X') { | |
// Strike! | |
frame_score += 10; | |
} else { | |
if(!isNaN(parseInt(spare_ball_1))) { | |
frame_score += parseInt(spare_ball_1); | |
} | |
} | |
} else { | |
if(!isNaN(parseInt(ball_1))) { | |
frame_score += parseInt(ball_1); | |
} | |
if(!isNaN(parseInt(ball_2))) { | |
frame_score += parseInt(ball_2); | |
} | |
} | |
} | |
return frame_score; | |
} | |
var current_frame = 1; | |
var current_ball = 1; | |
var b_body = B('body'); | |
// We use borders | |
var border_string = '1px solid black'; | |
var full_border = {'border':border_string}; | |
// Build the score input | |
var sc_form = B('<form>',{'action':'javascript:void(0);'}) | |
.on('submit',function(){ | |
// On submitting the form | |
var c_frame_score = parseInt(B('#frame_'+current_frame+'_score').text()); | |
var v = parseInt(B('#score_input').val()); | |
if(isNaN(v) || v < 0 || v > 10 || (current_frame < 10 && (v + c_frame_score) > 10)) { | |
output('Invalid Score Submitted!','error'); | |
B('#score_input').val(''); | |
return; | |
} | |
// Validate the input | |
// Update the score table | |
if(v == 10 || (v + c_frame_score == 10)) { | |
if(current_ball == 1 | |
|| (current_frame == 10 && current_ball == 2 && (v + c_frame_score == 20)) | |
|| (current_frame == 10 && current_ball == 3 && (v + c_frame_score == 30))) { | |
B('#frame_'+current_frame+'_ball_'+current_ball).text('X'); | |
} else { | |
B('#frame_'+current_frame+'_ball_'+current_ball).text('/'); | |
} | |
} else { | |
B('#frame_'+current_frame+'_ball_'+current_ball).text(v); | |
} | |
B('#frame_'+current_frame+'_score').text(v + c_frame_score); | |
var game_running = false; | |
if(current_frame == 10) { | |
// TODO: There can be more than 2 balls on frame 10 | |
if(current_ball == 1 || | |
(current_ball == 2 && (v + c_frame_score > 10))) { | |
current_ball++; | |
game_running = true; | |
} | |
} else { | |
if(current_ball == 2 || v == 10) { | |
current_frame++; | |
current_ball = 1; | |
} else { | |
current_ball++; | |
} | |
game_running = true; | |
} | |
// Now update all frame totals, fixing spares and strikes | |
var running_score = 0; | |
for(var i = 1; i <= 10; i++) { | |
var calc_frame_score = calculate_frame_score(i); | |
running_score += calc_frame_score; | |
B('#frame_'+i+'_score').text(calc_frame_score); | |
B('#frame_'+i+'_running_score').text(running_score); | |
} | |
if(game_running) { | |
// Set the new input text | |
B('#score_text').text('Frame '+current_frame+', Ball '+current_ball+': '); | |
// And clear the input | |
B('#score_input').val(''); | |
} else { | |
B('#score_text').text('Game Complete!'); | |
B('#score_input').attr('disabled','disabled'); | |
B('#score_submit').attr('disabled','disabled'); | |
} | |
}); | |
var sc_span = B('<span>',{id:'score_text'}).text('Frame '+current_frame+', Ball '+current_ball+': '); | |
var sc_inp = B('<input>',{id:'score_input'}); | |
var sc_sbt = B('<input>',{id:'score_submit',type:'submit'}); | |
sc_form.append(sc_span); | |
sc_form.append(sc_inp); | |
sc_form.append(sc_sbt); | |
b_body.append(sc_form); | |
var user_message = B('<span>',{id:'user_message'}); | |
b_body.append(user_message); | |
// Ok, Let's build the scores table | |
tbl = B('<table>',{id:'scores_table'}).css(full_border); | |
// The header row | |
var tr = B('<tr>').css({'border-bottom':border_string}); | |
for(var i = 1; i <= 10; i++) { | |
if(i > 1) { | |
tr.append(B('<th>').text('Frame '+i).css({ | |
'border-left':border_string, | |
'padding':'0 5px' | |
})); | |
} else { | |
tr.append(B('<th>').text('Frame '+i).css({ | |
'padding':'0 5px' | |
})); | |
} | |
} | |
tbl.append(tr); | |
// Now the actual frames/ball scores row | |
var tr = B('<tr>'); | |
for(var i = 1; i <= 10; i++) { | |
var div_width = (i==10)?"30%":"40%"; | |
var td = B('<td>',{id:'frame_'+i}).css(full_border).css({'text-align':'center'}); | |
td.append(B('<div>',{id:'frame_'+i+'_ball_1'}).css(full_border).css({ | |
width:div_width, | |
display:'inline-block', | |
'text-align':'center' | |
})); | |
td.append(B('<div>',{id:'frame_'+i+'_ball_2'}).css(full_border).css({ | |
width:div_width, | |
display:'inline-block', | |
'text-align':'center' | |
})); | |
if(i==10) { | |
td.append(B('<div>',{id:'frame_'+i+'_ball_3'}).css(full_border).css({ | |
width:div_width, | |
display:'inline-block', | |
'text-align':'center' | |
})); | |
} | |
tr.append(td); | |
} | |
tbl.append(tr); | |
// Here we have a frame score row | |
var tr = B('<tr>'); | |
for(var i = 1; i <= 10; i++) { | |
var td = B('<td>',{id:'frame_'+i}).css(full_border); | |
td.append(B('<div>',{id:'frame_'+i+'_score'}).css({width:'100%',height:'100%',display:'inline-block','text-align':'center'}).text('0')); | |
tr.append(td); | |
} | |
tbl.append(tr); | |
// And finally the running score row | |
var tr = B('<tr>'); | |
for(var i = 1; i <= 10; i++) { | |
var td = B('<td>',{id:'frame_running_'+i}).css(full_border); | |
td.append(B('<div>',{id:'frame_'+i+'_running_score'}).css({width:'100%',height:'100%',display:'inline-block','text-align':'center'}).text('0')); | |
tr.append(td); | |
} | |
tbl.append(tr); | |
b_body.append(tbl); | |
var reset_btn = B('<button>').on('click',function(){location.reload();}).text('Reset'); | |
b_body.append(reset_btn); | |
/* Output method, the intention being that it could be expanded to show | |
* the User what's up. | |
*/ | |
function output(s, c) { | |
B('#user_message').text(s).removeClass('error').removeClass('success').addClass(c); | |
console.log(s); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nicely done!