Skip to content

Instantly share code, notes, and snippets.

@br0xen
Last active August 29, 2015 13:57
Show Gist options
  • Save br0xen/9507533 to your computer and use it in GitHub Desktop.
Save br0xen/9507533 to your computer and use it in GitHub Desktop.
devict.org Code Challenge - Bowling Score
/* 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);
}
@sethetter
Copy link

Nicely done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment