Skip to content

Feat/2023 02 #245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions 2023/day-02/game.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const parseGame = (gameString) => {
const data = gameString.split(':')
const id = parseInt(
data[0].match(/\d+/)[0] // find the game number
)
const draws = data[1].split(';') // split the game into draws
.map((draw) => {
const result = ['red', 'green', 'blue']
.map((color) => { // extract count for each color
const reg = new RegExp(`\\d+(?= ${color})`)
console.debug(reg)
const val = draw.match(reg) || [0]
console.debug(`${color} ${val}`)
return parseInt(val).toString(16).padStart(2, '0') // convert to hex
})

return result.join('') // combine into a RGB hex color string
})

return {
id,
draws
}
}

const parseHex = (hex) => {
return {
r: parseInt(hex.substring(0, 2), 16),
g: parseInt(hex.substring(2, 4), 16),
b: parseInt(hex.substring(4, 6), 16)
}
}

const validateDraw = (draw, limit) => {
const data = parseHex(draw)
const lim = parseHex(limit)
return (data.r <= lim.r && data.g <= lim.g && data.b <= lim.b)
}

const validateGame = (game, limit) => {
// const lim = parseHex(limit)
// const tally = game.draws.reduce((acc, draw) => {
// const drawData = parseHex(draw)
// return {
// r: acc.r + drawData.r,
// g: acc.g + drawData.g,
// b: acc.b + drawData.b
// }
// }, { r: 0, g: 0, b: 0 })

// const result = (tally.r <= lim.r && tally.g <= lim.g && tally.b <= lim.b)
// console.debug(`Game ${game.id} ${(result) ? 'passes' : 'fails'}`)
// if (!result) {
// console.debug(tally)
// }

// If any draw fails, the full game fails
const result = game.draws.reduce((res, draw) => {
return (res && validateDraw(draw, limit))
}, true)
return result
}

const checksumGameSet = (games, limit) => {
// tally the IDs of valid games
return games.reduce((acc, game) => {
return validateGame(game, limit) ? acc + game.id : acc
}, 0)
}

const countCubesNeeded = (game) => {
const max = game.draws.reduce((acc, draw) => {
const drawData = parseHex(draw)
return {
r: Math.max(acc.r, drawData.r),
g: Math.max(acc.g, drawData.g),
b: Math.max(acc.b, drawData.b)
}
}, { r: 0, g: 0, b: 0 })

return max
}

const power = (game) => {
const needed = countCubesNeeded(game)
return needed.r * needed.g * needed.b
}

module.exports = {
parseGame,
validateGame,
checksumGameSet,
validateDraw,
countCubesNeeded,
power
}
228 changes: 228 additions & 0 deletions 2023/day-02/game.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/* eslint-env mocha */
const { expect } = require('chai')
const { parseGame, validateGame, checksumGameSet, validateDraw, countCubesNeeded, power } = require('./game')
const { linesToArray } = require('../../2018/inputParser')
const fs = require('fs')
const path = require('path')
const filePath = path.join(__dirname, 'input.txt')

describe('--- Day 2: Cube Conundrum ---', () => {
describe('Part 1', () => {
describe('parseGame', () => {
it('extracts a game string into a data object with RGB hex values for draws', () => {
const data = [
'Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green',
'Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue',
'Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red',
'Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red',
'Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green'
]
const result = [
{
id: 1,
draws: [
'040003',
'010206',
'000200'
]
}, {
id: 2,
draws: [
'000201',
'010304',
'000101'
]
}, {
id: 3,
draws: [
'140806',
'040d05',
'010500'
]
}, {
id: 4,
draws: [
'030106',
'060300',
'0e030f'
]
}, {
id: 5,
draws: [
'060301',
'010202'
]
}
]

data.forEach((game, idx) => {
expect(parseGame(game)).to.deep.equal(result[idx])
})
})
})

describe('validateGame', () => {
it('checks if the game is valid given the limits', () => {
const limits = '0c0d0e' // 12 red cubes, 13 green cubes, and 14 blue cubes
const data = [
{
id: 1,
draws: [
'040003',
'010206',
'000200'
]
}, {
id: 2,
draws: [
'000201',
'010304',
'000101'
]
}, {
id: 3,
draws: [
'140806',
'040d05',
'010500'
]
}, {
id: 4,
draws: [
'030106',
'060300',
'0e030f'
]
}, {
id: 5,
draws: [
'060301',
'010202'
]
}
]
const result = [true, true, false, false, true]
data.forEach((game, idx) => {
expect(validateGame(game, limits)).to.equal(result[idx])
})
})
})

describe('checksumGameSet', () => {
it('tallies the IDs of valid games', () => {
const limits = '0c0d0e' // 12 red cubes, 13 green cubes, and 14 blue cubes
const data = [
{
id: 1,
draws: [
'040003',
'010206',
'000200'
]
}, {
id: 2,
draws: [
'000201',
'010304',
'000101'
]
}, {
id: 3,
draws: [
'140806',
'040d05',
'010500'
]
}, {
id: 4,
draws: [
'030106',
'060300',
'0e030f'
]
}, {
id: 5,
draws: [
'060301',
'010202'
]
}
]

expect(checksumGameSet(data, limits)).to.equal(8)
})
})

describe('validateDraw', () => {
it('validates an individual draw is within limits', () => {
const limit = '0c0d0e'
expect(validateDraw('010206', limit)).to.equal(true)
expect(validateDraw('060301', limit)).to.equal(true)
expect(validateDraw('040d05', limit)).to.equal(true)
expect(validateDraw('140806', limit)).to.equal(false) // game 3 draw 1 has 20 reds
expect(validateDraw('0e030f', limit)).to.equal(false) // game 4 draw 3 has 15 blues
})
})

describe.skip('integration test', () => {
let initData
before((done) => {
fs.readFile(filePath, { encoding: 'utf8' }, (err, rawData) => {
if (err) throw err
initData = linesToArray(rawData.trim()).map(parseGame)
// Deep copy to ensure we aren't mutating the original data
// data = JSON.parse(JSON.stringify(initData))
done()
})
})

it('result matches what we know about the answer', () => {
const limit = [12, 13, 14] // 12 red, 13 green, 14 blue
.map((num) => num.toString(16).padStart(2, '0'))
.join('')

expect(checksumGameSet(initData, limit)).to.be.gt(177) // 177 is too low
expect(checksumGameSet(initData, limit)).to.be.gt(1452) // 1452 (from creating the limit in hex wrong, and assuming cubes are not returned to the bag after each draw) is too low
})
})
})

describe('Part 2', () => {
describe('countCubesNeeded', () => {
it('counts how many cubes are needed for a game', () => {
const data = [
'Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green',
'Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue',
'Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red',
'Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red',
'Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green'
]
const result = [
{ r: 4, g: 2, b: 6 },
{ r: 1, g: 3, b: 4 },
{ r: 20, g: 13, b: 6 },
{ r: 14, g: 3, b: 15 },
{ r: 6, g: 3, b: 2 }
]
data.forEach((game, idx) => {
expect(countCubesNeeded(parseGame(game))).to.deep.equal(result[idx])
})
})
})
describe('power', () => {
it('calculates the power for a game', () => {
const data = [
'Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green',
'Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue',
'Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red',
'Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red',
'Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green'
]
const result = [48, 12, 1560, 630, 36]
data.forEach((game, idx) => {
expect(power(parseGame(game))).to.equal(result[idx])
})
})
})
})
})
3 changes: 3 additions & 0 deletions 2023/day-02/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eslint-disable-next-line no-unused-vars
const console = require('../helpers')
require('./solution')
Loading