Files
tps/src/modules/feature/feature-board.ts
2023-07-27 01:24:38 +02:00

293 lines
6.7 KiB
TypeScript

import { Feature } from '@/modules/feature/feature'
import { FeatureStep } from '@/modules/feature/feature-steps'
import { features as initialFeatures } from '@/modules/feature/feature.fixture'
import { Strategy } from '@/modules/lean/strategy'
import { FeatureState } from '@/store-type'
import {
getMean,
pickRandomElement,
popNElement,
shuffleArray,
sumElements
} from '@/utils'
const HARD_STOP = 5000
const hasQualityIssue = ({
complexity,
tasksInParallel,
teamWorkExperience
}: {
complexity: number
tasksInParallel: number
teamWorkExperience: number
}): boolean => {
const qualityProbability = getQualityProbability(
complexity,
teamWorkExperience
)
const multiplicator = getOverburdenMultiplicator(tasksInParallel)
const quality = Math.random()
return quality > qualityProbability / multiplicator
}
export const newBacklog = (limit?: number) =>
limit !== undefined
? popNElement(shuffleArray(initialFeatures), limit)
: shuffleArray(initialFeatures)
export const initBoard = (
steps: FeatureStep[],
features: Feature[]
): Feature[] => {
const initialFeatures = popNElement(features, 10)
initialFeatures.forEach((feature) => {
const step = pickRandomElement(steps)
feature.status = pickRandomElement(['doing', 'done'])
feature.step = step.stepIndex
})
return initialFeatures
}
export const getFeaturesForNextDay = ({
backlog,
features,
initialStep,
steps,
strategy,
teamWorkExperience
}: {
backlog: Feature[]
features: Feature[]
steps: FeatureStep[]
initialStep: number
strategy: Strategy | 'problem-solving'
teamWorkExperience: number
}): [Feature[], Feature[]] => {
features
.filter((feature) => feature.step > 0 || feature.status === 'doing')
.forEach((feature) => {
feature.leadTime++
if (strategy === 'problem-solving') {
return
}
switch (feature.status) {
case 'doing':
feature.status = 'done'
break
case 'done':
if (strategy === 'pull') {
const nextStep = steps.find(
(step) => step.stepIndex === feature.step - 1
)
if (!nextStep) {
break
}
const hasBlueBinAvailableNextStep =
nextStep.blueBins -
features.filter(
(f) => f.step === feature.step - 1 && f.status === 'done'
).length >
0
if (hasBlueBinAvailableNextStep) {
feature.status = 'doing'
}
} else {
feature.status = 'doing'
}
if (feature.status === 'done') {
break
}
if (
hasQualityIssue({
complexity: feature.complexity,
tasksInParallel: features.filter(
(f) => f.status === 'doing' && f.step === feature.step
).length,
teamWorkExperience
})
) {
feature.qualityIssue++
} else {
feature.step--
}
break
}
})
if (backlog.length > 0) {
switch (strategy) {
case 'push': {
const [nextFeature] = popNElement(backlog, 1)
if (nextFeature) {
features.push({ ...nextFeature, step: initialStep })
}
break
}
case 'pull': {
const firstStep = steps.find((step) => step.stepIndex === initialStep)
if (!firstStep) {
break
}
const hasBlueBinAvailableOnFirstStep =
firstStep.blueBins -
features.filter(
(f) => f.step === initialStep && f.status === 'done'
).length >
0
if (hasBlueBinAvailableOnFirstStep) {
const [newFeature] = popNElement(backlog, 1)
if (newFeature) {
features.push({ ...newFeature, step: initialStep })
}
}
}
}
}
return [backlog, features]
}
const getOverburdenMultiplicator = (tasksInParallel: number) => {
switch (tasksInParallel) {
case 1:
return 1
case 2:
return 1.2
case 3:
return 1.5
case 4:
return 1.8
case 5:
return 2.6
case 6:
return 4
case 7:
return 5.5
case 8:
return 7
default:
return 10
}
}
const getQualityProbability = (
complexity: number,
teamWorkExperience: number
) => {
let probabilityOfGoodQuality = 1
switch (complexity) {
case 1:
probabilityOfGoodQuality = 0.97
case 2:
probabilityOfGoodQuality = 0.9
break
case 3:
probabilityOfGoodQuality = 0.82
break
case 4:
probabilityOfGoodQuality = 0.71
break
case 5:
probabilityOfGoodQuality = 0.6
break
}
// team learning
probabilityOfGoodQuality =
probabilityOfGoodQuality + (1.2 * teamWorkExperience) / 100
return probabilityOfGoodQuality
}
export const isFeatureDone = (feature: Feature) =>
feature.step === 0 && feature.status === 'done'
export const nextDay = (
state: FeatureState,
strategy: Strategy | 'problem-solving'
): FeatureState => {
state.meta.totalDays++
if (strategy === 'problem-solving') {
const hasTeamLearned = Math.random() > 0.25
if (hasTeamLearned) {
state.meta.teamWorkExperience++
}
} else {
state.meta.strategy[strategy]++
}
const [backlog, features] = getFeaturesForNextDay({
backlog: state.backlog,
features: state.features,
steps: state.steps,
initialStep: state.steps[0].stepIndex,
strategy,
teamWorkExperience: state.meta.teamWorkExperience
})
state.backlog = backlog
state.features = features
const featuresDone = sumElements(state.meta.featuresDonePerDay) ?? 0
const featuresDoneNextDay = features.filter(isFeatureDone).length
state.meta.featuresDonePerDay.push(featuresDoneNextDay - featuresDone)
return state
}
export const isProjectFinished = (features: Feature[]) =>
features.every(isFeatureDone)
export const getMeanComplexity = (features: Feature[]) => {
return getMean(features.map((feature) => feature.complexity))
}
export const getMeanLeadTime = (features: Feature[]) => {
return getMean(features.map((feature) => feature.leadTime))
}
export const getMeanQualityIssue = (features: Feature[]) => {
return getMean(features.map((feature) => feature.qualityIssue))
}
export const simulate = (
state: FeatureState,
strategy: Strategy
): FeatureState => {
let i = 0
while (!isProjectFinished(state.features) && i++ < HARD_STOP) {
if (strategy.includes('dps')) {
if (state.meta.totalDays % 5 === 0) {
state = nextDay(state, 'problem-solving')
} else {
state = nextDay(state, strategy.split('-')[0] as Strategy)
}
} else {
state = nextDay(state, strategy)
}
}
return state
}