Add tests for canLearnTalent and canUnlearnTalent

This commit is contained in:
Melvin Valster
2019-07-23 23:44:36 +02:00
parent c4bec9bea9
commit d5121960a1
9 changed files with 234 additions and 109 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ body {
&__class { &__class {
margin-right: 1em; margin-right: 1em;
opacity: .8; opacity: 1;
transition: all .1s ease-out; transition: all .1s ease-out;
&:hover { &:hover {
+2 -1
View File
@@ -5,7 +5,8 @@ import { BrowserRouter as Router, Route } from 'react-router-dom'
const App: React.FC = () => { const App: React.FC = () => {
return ( return (
<Router basename={process.env.NODE_ENV !== 'development' ? '%PUBLIC_URL%' : ''}> <Router basename={process.env.NODE_ENV === 'production' ? '/wow-talent-calculator' : ''}>
{/* <Router basename={process.env.NODE_ENV !== 'development' ? '%PUBLIC_URL%' : ''}> */}
<div className="App"> <div className="App">
<Route path="/:selectedClass?/:pointString?" component={IndexRoute} /> <Route path="/:selectedClass?/:pointString?" component={IndexRoute} />
</div> </div>
+47 -11
View File
@@ -1,5 +1,21 @@
@import "../sass/config"; @import "../sass/config";
@function baseRowTopOffset($row) {
@return $row-offset + ($row * $row-distance);
}
@function calcLeftOffset($col) {
@return $col-offset + ($col-gutter * ($col - 1)) + $icon-size;
}
@function calcRightOffset($col) {
@return $col-offset + ($col-distance * $col) + $icon-size;
}
@function calcArrowHeight($length) {
@return 2px + ($row-offset * $length) + ($icon-size * ($length - 1))
}
.arrow { .arrow {
$arrow-width: 15px; $arrow-width: 15px;
position: absolute; position: absolute;
@@ -11,7 +27,7 @@
// Rows // Rows
@for $i from 0 through 6 { @for $i from 0 through 6 {
&[data-row="#{$i}"] { &[data-row="#{$i}"] {
top: 12px + $row-offset + (($i) * $row-distance); top: 12px + baseRowTopOffset($i);
} }
} }
@@ -24,50 +40,50 @@
} }
&--right { &--right {
background-image: url('/images/arrows/right.png'); background-image: url('../images/arrows/right.png');
background-position: center right; background-position: center right;
&.arrow--active { &.arrow--active {
background-image: url('/images/arrows/right-active.png'); background-image: url('../images/arrows/right-active.png');
} }
// Cols // Cols
@for $i from 0 through 3 { @for $i from 0 through 3 {
&[data-col="#{$i}"] { &[data-col="#{$i}"] {
left: 3px + $col-offset + ($col-distance * $i) + $icon-size; left: 3px + calcRightOffset($i);
} }
} }
} }
&--left { &--left {
background-image: url('/images/arrows/left.png'); background-image: url('../images/arrows/left.png');
background-position: center left; background-position: center left;
&.arrow--active { &.arrow--active {
background-image: url('/images/arrows/left-active.png'); background-image: url('../images/arrows/left-active.png');
} }
// Cols // Cols
@for $i from 0 through 3 { @for $i from 0 through 3 {
&[data-col="#{$i}"] { &[data-col="#{$i}"] {
left: -3px + $col-offset + ($col-gutter * ($i - 1)) + $icon-size; left: -3px + calcLeftOffset($i);
} }
} }
} }
&--down { &--down {
width: $arrow-width; width: $arrow-width;
background-image: url('/images/arrows/down.png'); background-image: url('../images/arrows/down.png');
background-position: center bottom; background-position: center bottom;
&.arrow--active { &.arrow--active {
background-image: url('/images/arrows/down-active.png'); background-image: url('../images/arrows/down-active.png');
} }
// Rows // Rows
@for $i from 0 through 6 { @for $i from 0 through 6 {
&[data-row="#{$i}"] { &[data-row="#{$i}"] {
top: $row-offset + (($i) * $row-distance) + 40px; top: 40px + baseRowTopOffset($i);
} }
} }
@@ -81,8 +97,28 @@
// Lengths // Lengths
@for $i from 0 through 3 { @for $i from 0 through 3 {
&[data-length="#{$i}"] { &[data-length="#{$i}"] {
height: 2px + ($row-offset * $i) + ($icon-size * ($i - 1)); height: calcArrowHeight($i);
} }
} }
} }
&--right-down {
// Horizontal
::before {
content: "";
position: absolute;
height: $arrow-width;
background-image: url('../images/arrows/rightdown.png');
background-position: center right;
}
// Vertical
::after {
content: "";
position: absolute;
width: $arrow-width;
background-image: url('../images/arrows/down.png');
background-position: center bottom;
}
}
} }
+11 -4
View File
@@ -4,11 +4,14 @@ import { TalentTree } from './TalentTree'
import { import {
modifyTalentPoint, modifyTalentPoint,
calcAvailablePoints, calcAvailablePoints,
encodeKnownTalents encodeKnownTalents,
SORT_TALENTS_BY_SPEC
} from '../lib/tree' } from '../lib/tree'
import { talentsBySpec } from '../data/talents' import { talentsBySpec, talentsById } from '../data/talents'
import { classByName } from '../data/classes' import { classByName } from '../data/classes'
import { History } from 'history' import { History } from 'history'
import { spells } from '../data/spells'
import { debugPrintKnown } from '../lib/debug'
interface Props { interface Props {
selectedClass: string selectedClass: string
@@ -56,13 +59,16 @@ export class Calculator extends React.PureComponent<Props> {
handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => { handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => {
const talent = talentsBySpec[specId][talentId] const talent = talentsBySpec[specId][talentId]
console.log('Clicked talent: ' + talentId) console.log('Clicked talent: ', talentId)
const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier) const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier)
if (newKnownTalents !== this.state.knownTalents) { if (newKnownTalents !== this.state.knownTalents) {
this.updateURL(newKnownTalents) this.updateURL(newKnownTalents)
} }
this.setState({ knownTalents: newKnownTalents }) this.setState({ knownTalents: newKnownTalents })
// Debug
debugPrintKnown(newKnownTalents)
} }
render() { render() {
@@ -93,7 +99,8 @@ export class Calculator extends React.PureComponent<Props> {
<ul> <ul>
<li><a href="/shaman/-5505000055523051-55">Shaman test</a></li> <li><a href="/shaman/-5505000055523051-55">Shaman test</a></li>
<li><a href="/shaman/-5595000055523051-55">Shaman test broken</a></li> <li><a href="/shaman/-5595000055523051-55">Shaman test broken</a></li>
<li><a href="/rogue/-005055-50205302332212051">Rogue can unlearn first row</a></li> <li><a href="/rogue/-005055-50205302332212051">Rogue (should break, does not meet requirement)</a></li>
<li><a href="/rogue/-005055-50205302333212041">Rogue can unlearn first row AND dependency</a></li>
</ul> </ul>
</div> </div>
) )
View File
+11
View File
@@ -0,0 +1,11 @@
import './Tooltip.scss'
import React, { FC } from 'react'
interface Props {
}
export const Tooltip: FC<Props> = (props) => {
return <div className="tooltip">
</div>
}
+13
View File
@@ -0,0 +1,13 @@
import { Map } from 'immutable'
import { SORT_TALENTS_BY_SPEC } from './tree'
import { talentsById } from '../data/talents'
export const debugPrintKnown = (known: Map<number, number>) => {
const obj = {}
known.toArray()
.map(([talentId]) => talentsById[talentId])
.sort(SORT_TALENTS_BY_SPEC)
.forEach(talent => { obj[talent.id] = known.get(talent.id) })
console.log(JSON.stringify(obj, null, 2))
}
+89 -35
View File
@@ -1,40 +1,94 @@
import im from 'immutable' import im, {
import { Map
// setTalentPointsInTree, } from 'immutable'
getPointsInSpec import {
canUnlearnTalent, canLearnTalent
} from './tree' } from './tree'
import {
talentsById
} from '../data/talents'
const createKnownTalents = (obj: object): Map <number, number> => {
let m = Map<number, number>()
Object.keys(obj).forEach((key) => {
m = m.set(parseInt(key, 10), obj[key])
})
return m
}
// describe('setTalentPointsInTree', () => { const ROGUE_TALENTS = createKnownTalents({
// it('sets points on an empty tree', () => { 241: 5, // Master of Deception
// const tree = im.List() 181: 5,
// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5]) 186: 5,
// }) 187: 5,
244: 5,
// it('sets points in the end of the current range', () => { 245: 3,
// const tree = im.List([0, 1]) 246: 3,
// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 1, 5]) 262: 3,
// }) 263: 2,
265: 2,
// it('sets points in the middle of the current range', () => { 284: 1,
// const tree = im.List([0, 0, 0, 0, 0, 0, 5]) 381: 1,
// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5, 0, 0, 0, 5]) 1123: 3,
// }) 1700: 2,
1701: 2,
// it('does not mutate the tree for points already set', () => { 1702: 5
// const tree = im.List([0, 3, 2, 0, 5]) })
// expect(setTalentPointsInTree(tree, 1, 3)).toStrictEqual(tree)
// })
// })
// describe('getTreePointCount', () => { describe('canUnlearnTalent', () => {
// it('returns proper count', () => { it('returns false for the incorrect Rogue talent case', () => {
// const result = getPointsInSpec(im.List([0, 0, 4, 5, 3, 0, 0])) const result = canUnlearnTalent(ROGUE_TALENTS, talentsById[241])
// expect(result).toBe(12) expect(result).toBe(false)
// }) })
// it('returns 0 for empty list', () => { it('returns false if no points are spent for the talent', () => {
// const result = getPointsInSpec(im.List()) const result = canUnlearnTalent(Map(), talentsById[241])
// expect(result).toBe(0) expect(result).toBe(false)
// }) })
// })
it('returns false if the talent is a dependency for another learnt talent', () => {
// http://localhost:3000/rogue/-00505001
const known = createKnownTalents({
186: 5, // Row 1
187: 5, // Row 2
301: 1, // Row 3
})
const result = canUnlearnTalent(known, talentsById[187])
expect(result).toBe(false)
})
it('returns true with points only spent in the first row', () => {
const known = createKnownTalents({
241: 5
})
const result = canUnlearnTalent(known, talentsById[241])
expect(result).toBe(true)
})
})
describe('canLearnTalent', () => {
it('returns false if 51 points are already spent', () => {
// http://localhost:3000/rogue/-005055010055505-55005
const known = createKnownTalents({
"181": 5,
"182": 5,
"184": 5,
"186": 5,
"187": 5,
"221": 5,
"222": 1,
"241": 5,
"242": 4,
"244": 5,
"261": 5,
"301": 1
})
const result = canLearnTalent(known, talentsById[222])
expect(result).toBe(false)
})
it('returns true for talent in the first row', () => {
const result = canLearnTalent(Map(), talentsById[186])
expect(result).toBe(true)
})
})
+60 -57
View File
@@ -16,6 +16,15 @@ export const SORT_TALENTS = (a: TalentData, b: TalentData) => {
return a.row - b.row return a.row - b.row
} }
export const SORT_TALENTS_BY_SPEC = (a: TalentData, b: TalentData) => {
const aSpec = talentToSpec[a.id]
const bSpec = talentToSpec[b.id]
if (aSpec === bSpec) {
return SORT_TALENTS(a, b)
}
return aSpec - bSpec
}
/** /**
* Returns the overall points spent in the tree. * Returns the overall points spent in the tree.
*/ */
@@ -69,6 +78,56 @@ export const canLearnTalent = (known: Map<number, number>, talent: TalentData):
return true return true
} }
export const getCumulativePointsPerRow = (known: Map<number, number>, specId: number): number[] => {
return known.reduce((reduction, points, talentId) => {
const t = talentsBySpec[specId][talentId]
if (t && points > 0) {
for (let row = t.row; row < MAX_ROWS; row++) {
reduction[row] = (reduction[row] || 0) + points
}
}
return reduction
}, [])
}
export const canUnlearnTalent = (known: Map<number, number>, talent: TalentData): boolean => {
const currentPoints = known.get(talent.id, 0)
const specId = talentToSpec[talent.id]
// No points to reduce for this talent
if (currentPoints === 0) {
console.warn('no points to reduce')
return false
}
// Prevent if another talent depends on this
const isDependency = known.some((points, talentId) => {
const t = talentsBySpec[specId][talentId]
return t && points > 0 && t.requires.some((req) => req.id === talent.id)
})
if (isDependency) {
console.warn('is dependency')
return false
}
// Walk through every talent and ensure no requirements are breached
let cumulativePointsPerRow = getCumulativePointsPerRow(known, specId)
for (let r = talent.row; r < cumulativePointsPerRow.length; r++) {
// Calculate what the points would look like when this one is removed
cumulativePointsPerRow[r] = cumulativePointsPerRow[r] - 1
}
const wouldBreach = known.some((points, talentId) => {
const t = talentsBySpec[specId][talentId]
return t && points > 0 && t.row > 0 && cumulativePointsPerRow[t.row - 1] < t.row * 5
})
if (wouldBreach) {
console.warn('point requirements would be breached')
return false
}
return true
}
/** /**
* Adds a single talent point to the Map, if possible. * Adds a single talent point to the Map, if possible.
*/ */
@@ -87,48 +146,8 @@ export const addTalentPoint = (known: Map<number, number>, talent: TalentData):
*/ */
export const removeTalentPoint = (known: Map<number, number>, talent: TalentData): Map<number, number> => { export const removeTalentPoint = (known: Map<number, number>, talent: TalentData): Map<number, number> => {
const currentPoints = known.get(talent.id, 0) const currentPoints = known.get(talent.id, 0)
const specId = talentToSpec[talent.id]
// No points to reduce for this talent if (!canUnlearnTalent(known, talent)) {
if (currentPoints === 0) {
console.warn('no points to reduce')
return known
}
let isDependency = false
let highestRow = 0
let cumulativePointsPerRow = {}
known.forEach((points, talentId) => {
const t = talentsBySpec[specId][talentId]
if (t && points > 0) {
isDependency = isDependency || t.requires.some((req) => req.id === talent.id)
if (t.row > highestRow) {
console.info('new highest row:', t)
}
highestRow = t.row > highestRow ? t.row : highestRow
for (let row = t.row; row < MAX_ROWS; row++) {
cumulativePointsPerRow[row] = (cumulativePointsPerRow[row] || 0) + points
}
}
})
// Check if removing this talent would not break the requirements for talents spent in later rows
const pointsUntilHighestRow = cumulativePointsPerRow[highestRow - 1]
const targetPointsHighestRow = highestRow * 5
if (talent.row < highestRow && pointsUntilHighestRow - 1 < targetPointsHighestRow) {
console.warn('would not break the requirements for talents spent in later rows', {
talent,
highestRow,
pointsUntilHighestRow,
targetPointsHighestRow
})
return known
}
// Prevent if another talent depends on this
if (isDependency) {
console.warn('is dependency')
return known return known
} }
@@ -148,19 +167,6 @@ export const modifyTalentPoint = (known: Map<number, number>, talent: TalentData
} }
} }
// TODO
export function parsePointString(str: string): List<List<number>> {
const list: Array<number[]> = []
const trees = str.split('-')
trees.forEach((stringForTree, index) => {
const points = stringForTree.split('').map(a => parseInt(a, 10))
list[index] = points
})
return fromJS(list)
}
/** /**
* Encodes a Map of known talents into a URL-friendly string. * Encodes a Map of known talents into a URL-friendly string.
*/ */
@@ -183,8 +189,6 @@ export function encodeKnownTalents(known: Map<number, number>, className: string
* Decodes a string of points into a Map of talents. * Decodes a string of points into a Map of talents.
*/ */
export function decodeKnownTalents(pointString: string, className: string): Map<number, number> { export function decodeKnownTalents(pointString: string, className: string): Map<number, number> {
console.log(pointString, className)
const { specs } = classByName[className] const { specs } = classByName[className]
let known = Map<number, number>() let known = Map<number, number>()
@@ -206,7 +210,6 @@ export function decodeKnownTalents(pointString: string, className: string): Map<
} }
if (points > 0) { if (points > 0) {
console.log(`Spent ${points} in ${talent.id}`)
known = known.set(talent.id, points) known = known.set(talent.id, points)
} }
} }