Skip to content

Instantly share code, notes, and snippets.

@jomido
Last active February 5, 2025 21:18
Show Gist options
  • Save jomido/5969a670db7a4a0eaf94dc925aa9070d to your computer and use it in GitHub Desktop.
Save jomido/5969a670db7a4a0eaf94dc925aa9070d to your computer and use it in GitHub Desktop.
Simple American Football Score Simulator
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
* {
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
background: #012;
color: cyan;
font-family: Consolas, monospace;
user-select: none;
}
.root {
width: 100%;
height: 100%;
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-evenly;
flex-direction: column;
gap: 1rem;
}
.root.done {
background: rgba(0, 255, 155, 0.2);
}
.scores {
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
pointer-events: none;
}
.home, .away {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 1rem;
width: 5rem;
height: 5rem;
gap: 0.5rem;
background: rgba(0, 255, 255, 0.1);
border-radius: 50%;
border: 3px solid transparent;
transition: 0.3s ease-in-out transform;
/* hacks to fix blurry font on mobile while transforming */
/* backface-visibility: hidden;
-webkit-font-smoothing: subpixel-antialiased; */
}
.home.tied, .away.tied {
border: 3px solid rgba(0, 255, 255, 0.2);
}
.home.winning, .away.winning {
/* border: 3px solid cyan; */
background: rgba(0, 255, 255, 0.2);
}
.overtime {
background: rgba(0, 255, 255, 0.2);
padding: 1rem;
border-radius: 2rem;
opacity: 0;
}
.overtime.show {
opacity: 1;
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
gap: 1rem;
}
button {
border: 2px solid cyan;
outline: none;
padding: 1.5rem;
background: transparent;
color: cyan;
cursor: pointer;
user-select: none;
font-size: 1.5rem;
margin-top: auto;
}
button:hover {
background: rgba(0, 255, 255, 0.2);
}
button:active {
transform: translateY(1px);
background: rgba(0, 255, 255, 0.3);
}
button.next.disabled {
opacity: 0.2;
pointer-events: none;
}
</style>
<script>
// rendering
let raf = null
const withRender = (fn) => {
return (...args) => {
fn(...args)
if (!raf) {
raf = requestAnimationFrame(() => {
render(state)
raf = null
})
}
return update
}
}
const render = (state) => {
renderRoot(state)
renderHome(state)
renderAway(state)
renderOvertime(state)
renderButtons(state)
}
</script>
<script>
// actions
const actions = {
onNext: () => {
const index = state.scoreIndex + 1
update.scoreIndex(index)
},
onReset: () => {
const [scores, ot] = generateGame()
update.scores(scores).scoreIndex(0).overtime(ot)
if (ot) {
console.log("OT")
}
}
}
</script>
<script>
// state
const state = {
overtime: false,
scores: [],
scoreIndex: 0,
}
const update = {
overtime: withRender((ot) => {
state.overtime = ot
}),
scores: withRender((scores) => {
state.scores = scores
}),
scoreIndex: withRender((index) => {
state.scoreIndex = index
})
}
</script>
<script>
// outcome generator
const odds = {
none: 0.575,
fg: 0.3,
td: 0.125,
}
const pts = {
none: 0,
fg: 3,
td: 7,
}
const drives = 24
const generateGame = () => {
let s = [0, 0]
const g = [[0, 0]]
let side = Math.random() <= 0.5 ? 0 : 1
const run = () => {
const prob = Math.random()
let o
if (prob <= odds.td) {
o = 'td'
} else if (prob <= odds.td + odds.fg) {
o = 'fg'
} else {
o = 'none'
}
const p = pts[o]
if (p > 0) {
if (side === 0) {
s = [s[0] + p, s[1]]
}
else {
s = [s[0], s[1] + p]
}
g.push([s[0], s[1]])
}
side = side === 1 ? 0 : 1
}
Array.from({ length: drives }, () => {
run()
})
let last = g[g.length - 1]
let ot = false
while (last[0] === last[1]) {
ot = true
side = Math.random() <= 0.5 ? 0 : 1
run()
last = g[g.length - 1]
}
return [g, ot]
}
const pbp = () => {
const [g, ot] = generateGame()
g.forEach((score, i) => {
if (i === g.length - 1 && ot) {
console.log("------OT------")
}
console.log(`${i + 1}: ${score}`)
})
}
</script>
<script>
// DOM
const makeDom = () => {
const root = document.querySelector(".root")
const scores = document.querySelector(".scores")
const home = document.querySelector(".home")
const homeTitle = home.querySelector(".title")
const homeScore = home.querySelector(".score")
const away = document.querySelector(".away")
const awayTitle = away.querySelector(".title")
const awayScore = away.querySelector(".score")
const overtime = document.querySelector(".overtime")
const buttons = document.querySelector(".buttons")
const buttonNext = buttons.querySelector(".next")
const buttonReset = buttons.querySelector(".reset")
window.dom = {
root,
scores,
home,
homeTitle,
homeScore,
away,
awayTitle,
awayScore,
overtime,
buttons,
buttonNext,
buttonReset,
}
}
</script>
<script>
// DOM: home
const renderHome = (state) => {
const myLastScore = state.scores[state.scoreIndex - 1]?.[0]
const myScore = state.scores[state.scoreIndex][0]
const theirScore = state.scores[state.scoreIndex][1]
dom.homeScore.innerText = myScore
dom.home.style.transform = utils.transform(myScore)
if (myScore === 0 && theirScore === 0) {
dom.home.classList = 'home'
return
}
if (myScore > theirScore) {
dom.home.classList = `home winning`
} else if (myScore === theirScore) {
dom.home.classList = `home tied`
} else {
dom.home.classList = `home`
}
}
</script>
<script>
// DOM: root
const renderRoot = () => {
if (state.scoreIndex + 1 === state.scores.length) {
dom.root.classList = 'root done'
} else {
dom.root.classList = 'root'
}
}
</script>
<script>
// DOM: away
const renderAway = (state) => {
const myLastScore = state.scores[state.scoreIndex - 1]?.[1]
const myScore = state.scores[state.scoreIndex][1]
const theirScore = state.scores[state.scoreIndex][0]
dom.awayScore.innerText = myScore
dom.away.style.transform = utils.transform(myScore)
if (myScore === 0 && theirScore === 0) {
dom.away.classList = 'away'
return
}
if (myScore > theirScore) {
dom.away.classList = `away winning`
} else if (myScore === theirScore){
dom.away.classList = `away tied`
} else {
dom.away.classList = `away`
}
}
</script>
<script>
// DOM: overtime
const renderOvertime = (state) => {
if (state.scoreIndex + 2 >= state.scores.length && state.overtime) {
dom.overtime.classList = "overtime show"
} else {
dom.overtime.classList = 'overtime'
}
}
</script>
<script>
// DOM: buttons
const renderButtons = (state) => {
const next = dom.buttonNext
if (state.scoreIndex + 1 >= state.scores.length) {
next.classList = 'next disabled'
} else {
next.classList = 'next'
}
}
</script>
<script>
// utils
const utils = {
transform: (score) => {
const factor = score / 10 + 1
return `scale(${factor}) translateZ(0)`
}
}
</script>
<script>
// misc
/**
* Attempt to prevent dbl-tap zoom on mobile
*/
document.addEventListener(
"dblclick",
function (event) {
event.preventDefault();
},
{ passive: false }
);
</script>
<script>
// entrypoint
document.addEventListener('DOMContentLoaded', function() {
makeDom()
actions.onReset()
})
</script>
</head>
<body>
<div class="root">
<div class="scores">
<div class="home">
<div class="title">HOME</div>
<div class="score">0</div>
</div>
<div class="away">
<div class="title">AWAY</div>
<div class="score">0</div>
</div>
</div>
<div class="overtime">-*[ OVERTIME ]*-</div>
<div class="buttons">
<button class="next" onClick="actions.onNext()">next</button>
<button class="reset" onClick="actions.onReset()">reset</button>
</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment