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 {
margin-right: 1em;
opacity: .8;
opacity: 1;
transition: all .1s ease-out;
&:hover {
+2 -1
View File
@@ -5,7 +5,8 @@ import { BrowserRouter as Router, Route } from 'react-router-dom'
const App: React.FC = () => {
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">
<Route path="/:selectedClass?/:pointString?" component={IndexRoute} />
</div>
+47 -11
View File
@@ -1,5 +1,21 @@
@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-width: 15px;
position: absolute;
@@ -11,7 +27,7 @@
// Rows
@for $i from 0 through 6 {
&[data-row="#{$i}"] {
top: 12px + $row-offset + (($i) * $row-distance);
top: 12px + baseRowTopOffset($i);
}
}
@@ -24,50 +40,50 @@
}
&--right {
background-image: url('/images/arrows/right.png');
background-image: url('../images/arrows/right.png');
background-position: center right;
&.arrow--active {
background-image: url('/images/arrows/right-active.png');
background-image: url('../images/arrows/right-active.png');
}
// Cols
@for $i from 0 through 3 {
&[data-col="#{$i}"] {
left: 3px + $col-offset + ($col-distance * $i) + $icon-size;
left: 3px + calcRightOffset($i);
}
}
}
&--left {
background-image: url('/images/arrows/left.png');
background-image: url('../images/arrows/left.png');
background-position: center left;
&.arrow--active {
background-image: url('/images/arrows/left-active.png');
background-image: url('../images/arrows/left-active.png');
}
// Cols
@for $i from 0 through 3 {
&[data-col="#{$i}"] {
left: -3px + $col-offset + ($col-gutter * ($i - 1)) + $icon-size;
left: -3px + calcLeftOffset($i);
}
}
}
&--down {
width: $arrow-width;
background-image: url('/images/arrows/down.png');
background-image: url('../images/arrows/down.png');
background-position: center bottom;
&.arrow--active {
background-image: url('/images/arrows/down-active.png');
background-image: url('../images/arrows/down-active.png');
}
// Rows
@for $i from 0 through 6 {
&[data-row="#{$i}"] {
top: $row-offset + (($i) * $row-distance) + 40px;
top: 40px + baseRowTopOffset($i);
}
}
@@ -81,8 +97,28 @@
// Lengths
@for $i from 0 through 3 {
&[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 {
modifyTalentPoint,
calcAvailablePoints,
encodeKnownTalents
encodeKnownTalents,
SORT_TALENTS_BY_SPEC
} from '../lib/tree'
import { talentsBySpec } from '../data/talents'
import { talentsBySpec, talentsById } from '../data/talents'
import { classByName } from '../data/classes'
import { History } from 'history'
import { spells } from '../data/spells'
import { debugPrintKnown } from '../lib/debug'
interface Props {
selectedClass: string
@@ -56,13 +59,16 @@ export class Calculator extends React.PureComponent<Props> {
handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => {
const talent = talentsBySpec[specId][talentId]
console.log('Clicked talent: ' + talentId)
console.log('Clicked talent: ', talentId)
const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier)
if (newKnownTalents !== this.state.knownTalents) {
this.updateURL(newKnownTalents)
}
this.setState({ knownTalents: newKnownTalents })
// Debug
debugPrintKnown(newKnownTalents)
}
render() {
@@ -93,7 +99,8 @@ export class Calculator extends React.PureComponent<Props> {
<ul>
<li><a href="/shaman/-5505000055523051-55">Shaman test</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>
</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 {
// setTalentPointsInTree,
getPointsInSpec
import im, {
Map
} from 'immutable'
import {
canUnlearnTalent, canLearnTalent
} 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', () => {
// it('sets points on an empty tree', () => {
// const tree = im.List()
// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5])
// })
// it('sets points in the end of the current range', () => {
// const tree = im.List([0, 1])
// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 1, 5])
// })
// it('sets points in the middle of the current range', () => {
// const tree = im.List([0, 0, 0, 0, 0, 0, 5])
// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5, 0, 0, 0, 5])
// })
// it('does not mutate the tree for points already set', () => {
// const tree = im.List([0, 3, 2, 0, 5])
// expect(setTalentPointsInTree(tree, 1, 3)).toStrictEqual(tree)
// })
// })
const ROGUE_TALENTS = createKnownTalents({
241: 5, // Master of Deception
181: 5,
186: 5,
187: 5,
244: 5,
245: 3,
246: 3,
262: 3,
263: 2,
265: 2,
284: 1,
381: 1,
1123: 3,
1700: 2,
1701: 2,
1702: 5
})
// describe('getTreePointCount', () => {
// it('returns proper count', () => {
// const result = getPointsInSpec(im.List([0, 0, 4, 5, 3, 0, 0]))
// expect(result).toBe(12)
// })
describe('canUnlearnTalent', () => {
it('returns false for the incorrect Rogue talent case', () => {
const result = canUnlearnTalent(ROGUE_TALENTS, talentsById[241])
expect(result).toBe(false)
})
// it('returns 0 for empty list', () => {
// const result = getPointsInSpec(im.List())
// expect(result).toBe(0)
// })
// })
it('returns false if no points are spent for the talent', () => {
const result = canUnlearnTalent(Map(), talentsById[241])
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
}
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.
*/
@@ -69,6 +78,56 @@ export const canLearnTalent = (known: Map<number, number>, talent: TalentData):
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.
*/
@@ -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> => {
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 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')
if (!canUnlearnTalent(known, talent)) {
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.
*/
@@ -183,8 +189,6 @@ export function encodeKnownTalents(known: Map<number, number>, className: string
* Decodes a string of points into a Map of talents.
*/
export function decodeKnownTalents(pointString: string, className: string): Map<number, number> {
console.log(pointString, className)
const { specs } = classByName[className]
let known = Map<number, number>()
@@ -206,7 +210,6 @@ export function decodeKnownTalents(pointString: string, className: string): Map<
}
if (points > 0) {
console.log(`Spent ${points} in ${talent.id}`)
known = known.set(talent.id, points)
}
}