319 lines
7.3 KiB
TypeScript
319 lines
7.3 KiB
TypeScript
import { newsAppFeatures } from '@/data/app-feature'
|
|
import type {
|
|
Feature,
|
|
FeatureStatus
|
|
} from '@/modules/pull-system/feature/feature'
|
|
import type { FeatureStep } from '@/modules/pull-system/feature/feature-steps'
|
|
import {
|
|
birdFeatures,
|
|
mobileAppFeatures
|
|
} from '@/modules/pull-system/feature/feature.fixture'
|
|
import type { Strategy } from '@/modules/pull-system/lean/strategy'
|
|
import type { FeatureState } from '@/store-type'
|
|
import {
|
|
getMean,
|
|
pickRandomElement,
|
|
popNElement,
|
|
randomFloat,
|
|
shuffleArray,
|
|
sumElements
|
|
} from '@/utils'
|
|
|
|
const HARD_STOP = 5000
|
|
|
|
const getQualityIssue = ({
|
|
complexity,
|
|
tasksInParallel,
|
|
teamWorkExperience
|
|
}: {
|
|
complexity: number
|
|
tasksInParallel: number
|
|
teamWorkExperience: number
|
|
}): boolean => {
|
|
const qualityProbability = getQualityProbability(
|
|
complexity,
|
|
teamWorkExperience
|
|
)
|
|
|
|
const multiplicator = getOverburdenMultiplicator(tasksInParallel)
|
|
const quality = randomFloat(0, 1)
|
|
|
|
return quality > qualityProbability / multiplicator
|
|
}
|
|
|
|
const mayBeInProgress = ({
|
|
features,
|
|
feature,
|
|
strategy,
|
|
steps
|
|
}: {
|
|
features: Feature[]
|
|
feature: Feature
|
|
steps: FeatureStep[]
|
|
strategy: Strategy
|
|
}): FeatureStatus => {
|
|
if (strategy !== 'pull') {
|
|
return 'doing'
|
|
}
|
|
|
|
const nextStep = steps.find((step) => step.stepIndex === feature.step - 1)
|
|
|
|
if (!nextStep) {
|
|
return feature.status
|
|
}
|
|
|
|
const hasBlueBinAvailableNextStep =
|
|
nextStep.blueBins -
|
|
features.filter((f) => f.step === feature.step - 1).length >
|
|
0
|
|
|
|
if (hasBlueBinAvailableNextStep) {
|
|
return 'doing'
|
|
}
|
|
|
|
return feature.status
|
|
}
|
|
|
|
export const newBacklog = (type: 'bird' | 'mobile-app', limit?: number) => {
|
|
const initialFeatures =
|
|
type === 'bird' ? [...birdFeatures] : [...mobileAppFeatures]
|
|
return 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 = Math.max(step.stepIndex, 1)
|
|
})
|
|
|
|
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')
|
|
.sort((a, b) => (a.step < b.step ? -1 : 1))
|
|
.forEach((feature) => {
|
|
feature.leadTime++
|
|
|
|
if (strategy === 'problem-solving') {
|
|
return
|
|
}
|
|
|
|
switch (feature.status) {
|
|
case 'doing':
|
|
feature.status = 'done'
|
|
break
|
|
case 'done':
|
|
feature.status = mayBeInProgress({
|
|
features,
|
|
feature,
|
|
steps,
|
|
strategy
|
|
})
|
|
|
|
if (feature.status === 'done') {
|
|
break
|
|
}
|
|
|
|
const hasQualityIssue = getQualityIssue({
|
|
complexity: feature.complexity,
|
|
tasksInParallel: features.filter(
|
|
(f) => f.status === 'doing' && f.step === feature.step
|
|
).length,
|
|
teamWorkExperience
|
|
})
|
|
|
|
if (hasQualityIssue) {
|
|
feature.qualityIssue++
|
|
} else {
|
|
// moving to the next team
|
|
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.5
|
|
case 3:
|
|
return 5
|
|
case 4:
|
|
return 8
|
|
case 5:
|
|
return 13
|
|
default:
|
|
return 25
|
|
}
|
|
}
|
|
|
|
const getQualityProbability = (
|
|
complexity: number,
|
|
teamWorkExperience: number
|
|
) => {
|
|
let probabilityOfGoodQuality = 1
|
|
|
|
switch (complexity) {
|
|
case 1:
|
|
probabilityOfGoodQuality = 0.95
|
|
break
|
|
case 2:
|
|
probabilityOfGoodQuality = 0.88
|
|
break
|
|
case 3:
|
|
probabilityOfGoodQuality = 0.72
|
|
break
|
|
case 4:
|
|
probabilityOfGoodQuality = 0.65
|
|
break
|
|
case 5:
|
|
probabilityOfGoodQuality = 0.5
|
|
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++
|
|
// // each day, the teams know how to better work together
|
|
// state.meta.teamWorkExperience += 0.1
|
|
|
|
if (strategy === 'problem-solving') {
|
|
const hasTeamLearned = randomFloat(0, 1) > 0.25
|
|
if (hasTeamLearned) {
|
|
state.meta.teamWorkExperience += 1.2
|
|
}
|
|
} 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
|
|
}
|