import { randomItem, ordinalSuffix } from './utils.mjs'
import AminoAcid from './amino-acid.mjs'
import AminoAcidSelector from './amino-acid-selector.mjs'
import Die from './die.mjs'
import Error from './error.mjs'
import Genome from './genome.mjs'
import GenomeList from './genome-list.mjs'
import LethalitySelector from './lethality-selector.mjs'
import Nucleotide from './nucleotide.mjs'
import NucleotideSelector from './nucleotide-selector.mjs'
class CloneNucleotide {
constructor(rules) {
this.rules = rules
this.id = 'clone-nucleotide'
this._boundClickHandler = this.clickHandler.bind(this)
}
enter() {
this.rules.cloneButton.addEventListener('click',
this._boundClickHandler)
this.rules.cloneButton.disabled = false
}
exit() {
this.rules.cloneButton.removeEventListener('click',
this._boundClickHandler)
this.rules.cloneButton.disabled = true
}
clickHandler(evt) {
const genome = this.rules.currentGenome.clone()
this.rules.genomeList.push(genome)
this.rules.next(new RollForNucleotide(this.rules))
}
}
class RollForNucleotide {
constructor(rules) {
this.rules = rules
this.id = 'roll-for-nucleotide'
}
enter() {
this.rules.die.value = '--'
this.rules.die.onChanged = this.handleDieRoll.bind(this)
this.rules.die.enable()
}
exit() {
this.rules.die.disable()
this.rules.die.onChanged = undefined
}
handleDieRoll() {
if (this.rules.die.value > Rules.initialGenomeBases.length) {
this.rules.iterations--
if (this.rules.isLastIteration) {
this.rules.next(new DoNothing(this.rules))
} else {
this.rules.next(new CloneNucleotide(this.rules))
}
} else {
this.rules.next(new NucleotideSelect(this.rules))
}
}
}
class NucleotideSelect {
constructor(rules) {
this.rules = rules
this.id = 'nucleotide-select'
}
enter() {
this.want = this.rules.die.value
this.rules.instructions.querySelector('#select-number').innerHTML =
`${this.want}${ordinalSuffix(this.want)}`
this.rules.currentGenome.onNucleotideSelectionChanged =
this.handleSelectionChanged.bind(this)
this.rules.currentGenome.unlock()
}
exit() {
this.rules.currentGenome.lock()
this.rules.currentGenome.onNucleotideSelectionChanged = undefined;
}
handleSelectionChanged(nucleotide, i) {
i++;
if (i != this.rules.die.value) {
this.rules.error.innerHTML =
`You selected the ${i}${ordinalSuffix(i)} nucleotide. Please select the ${this.want}${ordinalSuffix(this.want)} one.`
this.rules.next(new ShowError(this.rules, this))
return
}
this.rules.next(new RollForMutation(this.rules))
}
}
class RollForMutation {
constructor(rules) {
this.rules = rules
this.id = 'roll-for-mutation'
}
enter() {
this.rules.die.value = '--'
this.rules.die.onChanged = this.handleDieRoll.bind(this)
this.rules.die.enable()
}
exit() {
this.rules.die.disable()
this.rules.die.onChanged = undefined
}
handleDieRoll() {
this.rules.next(new PerformMutation(this.rules))
}
}
class PerformMutation {
constructor(rules) {
this.rules = rules
this.id = 'perform-mutation'
}
enter() {
const selector = this.rules.nucleotideSelector
selector.onItemSelected = this.handleItemSelected.bind(this)
selector.attach(this.selectedNucleotide)
}
exit() {
this.rules.nucleotideSelector.detach()
}
validMutation(from, to) {
return to == this.expectedMutation[from]
}
get expectedMutation() {
if (this.rules.die.value <= 14) {
return Nucleotide.transition
} else if (this.rules.die.value <= 17) {
return Nucleotide.complementingTransversion
} else {
return Nucleotide.defaultTransversion
}
}
get selectedNucleotide() {
return this.rules.currentGenome.selectedNucleotide
}
get errorTransitionHTML() {
return `Select the base that corresponds to a transition of ${this.selectedNucleotide.value}.`
}
get errorComplementingTransversionHTML() {
return `Select the base that corresponds to a complementing transversion of ${this.selectedNucleotide.value}.`
}
get errorDefaultTransversionHTML() {
return `Select the base that corresponds to the other transversion of ${this.selectedNucleotide.value}.`
}
get errorHTML() {
if (this.expectedMutation == Nucleotide.transition) {
return this.errorTransitionHTML
} else if (this.expectedMutation == Nucleotide.complementingTransversion) {
return this.errorComplementingTransversionHTML
} else {
return this.errorDefaultTransversionHTML
}
}
handleItemSelected(base) {
if (!this.validMutation(this.selectedNucleotide.value, base)) {
this.rules.error.innerHTML = this.errorHTML
this.rules.next(new ShowError(this.rules, this))
return
}
this.rules.next(new SelectAminoAcid(this.rules))
}
}
class SelectAminoAcid {
constructor(rules) {
this.rules = rules
this.id ='amino-acid-select'
}
enter() {
window.t = this
const selector = this.rules.aminoAcidSelector
this.codon = this.rules.currentGenome.selectedCodon
this.expected = AminoAcid.codonMap[this.codon.value]
let x = selector.elt.querySelector('#amino-acid-selector .codon').innerHTML =
this.codon.value
selector.onItemSelected = this.handleItemSelected.bind(this)
selector.attach(this.selectedNucleotide)
this.rules.aminoAcidSelector.attach()
}
exit() {
this.rules.aminoAcidSelector.detach()
}
validSelection(aminoAcid) {
console.debug('aminoAcid:', aminoAcid, 'expected: ', this.expected)
return aminoAcid == this.expected
}
handleItemSelected(aminoAcid) {
if (!this.validSelection(aminoAcid)) {
this.rules.error.innerHTML =
`The codon ${this.codon.value} does not code for ${aminoAcid}`
this.rules.next(new ShowError(this.rules, this))
return
}
const newAminoAcid = new AminoAcid(...this.codon.value.split(''))
const isLethal = this.codon.aminoAcid.value !== newAminoAcid.value
this.codon.aminoAcid = newAminoAcid
this.rules.next(new MarkAsLethal(this.rules, isLethal))
}
}
class MarkAsLethal {
constructor(rules, isLethal) {
this.rules = rules
this.isLethal = isLethal
this.id = 'mark-as-lethal'
}
enter() {
this.rules.lethalitySelector.onItemSelected = this.handleItemSelected.bind(this)
this.rules.lethalitySelector.attach()
}
exit() {
this.rules.lethalitySelector.detach()
}
get lethalHTML() {
return 'A change in amino acid is a lethal change.'
}
get nonLethalHTML() {
return 'A change in amino acid is a lethal change.'
}
handleItemSelected(isLethal) {
if (isLethal !== this.isLethal) {
if (this.isLethal) {
this.rules.error.innerHTML = this.lethalHTML
} else {
this.rules.error.innerHTML = this.nonLethalHTML
}
this.rules.next(new ShowError(this.rules, this))
return
}
this.rules.iterations--
if (this.rules.isLastIteration) {
this.rules.next(new DoNothing(this.rules))
} else {
this.rules.next(new CloneNucleotide(this.rules))
}
}
}
class DoNothing {
constructor(rules) {
this.rules = rules
this.id = 'print-results'
this._boundClickHandler = this.clickHandler.bind(this)
}
enter() {
this.rules.printButton.addEventListener('click', this._boundClickHandler)
this.rules.printButton.disabled = false
}
exit() {
this.rules.printButton.disabled = true
this.rules.printButton.removeEventListener('click', this._boundClickHandler)
}
clickHandler() {
window.print()
}
}
class ShowError {
constructor(rules, nextState) {
this.rules = rules
this.nextState = nextState
this._boundClickHandler = this.clickHandler.bind(this)
}
enter() {
this.rules.error.onClick = this._boundClickHandler
this.rules.error.show()
}
exit() {
this.rules.error.hide()
this.rules.error.onClick = undefined
}
clickHandler() {
this.rules.next(this.nextState)
}
}
class Rules {
constructor(die, instructions, genomeList, aminoAcidSelector, nucleotideSelector, lethalitySelector, cloneButton, remainingIterations, printButton, errors) {
this.die = new Die(die)
this.instructions = instructions
this.genomeList = new GenomeList(genomeList)
this.aminoAcidSelector = new AminoAcidSelector(aminoAcidSelector)
this.nucleotideSelector = new NucleotideSelector(nucleotideSelector)
this.lethalitySelector = new LethalitySelector(lethalitySelector)
this.cloneButton = cloneButton
this.remainingIterations = remainingIterations
this.printButton = printButton
this.error = new Error(errors)
this.iterations = Rules.maxIterations
this.cloneButton.disabled = true
this.genomeList.push(new Genome(Rules.initialGenomeBases))
if (false) {
this._debugStartAtRollForMutation()
} else if (false) {
this._debugStartAtPerformMutation(3)
} else if (false) {
this._debugStartAtSelectAminoAcid()
} else if (true) {
this._debugStartAtSelectLethality()
} else if (false) {
this._debugStartWithError()
} else {
this.currentState = new CloneNucleotide(this)
}
this.enterState()
}
get iterations() {
return Number(this.remainingIterations.innerText)
}
set iterations(val) {
this.remainingIterations.innerText = val
}
get isLastIteration() {
return this.iterations == 0
}
_debugStartAtRollForMutation() {
this.genomeList.push(this.currentGenome.clone())
this.currentState = new RollForMutation(this)
const nucleotide = this.currentGenome.nucleotides[2]
this.currentGenome.selectedNucleotide = nucleotide
}
_debugStartAtPerformMutation(n) {
// The semicolon below is necessary to prevent the array
// expansion below it from being understood as an index
// operation.
this.genomeList.push(this.currentGenome.clone());
[...Array(n)].forEach(i => {
const n = randomItem(this.currentGenome.nucleotides)
n.value = randomItem(Nucleotide.bases)
this.currentGenome.selectedNucleotide = n
const g = this.currentGenome.clone()
this.genomeList.push(g)
})
this.currentState = new PerformMutation(this)
this.die.value = 15
const nucleotide = this.currentGenome.nucleotides[15]
this.currentGenome.selectedNucleotide = nucleotide
}
_debugStartAtSelectAminoAcid() {
this.genomeList.push(this.currentGenome.clone())
this.currentState = new SelectAminoAcid(this)
this.die.value = 15
const nucleotide = this.currentGenome.nucleotides[15]
nucleotide.value = 'A'
this.currentGenome.selectedNucleotide = nucleotide
}
_debugStartAtSelectLethality() {
this.genomeList.push(this.currentGenome.clone())
this.currentState = new MarkAsLethal(this, true)
this.die.value = 15
const nucleotide = this.currentGenome.nucleotides[15]
nucleotide.value = 'A'
this.currentGenome.selectedNucleotide = nucleotide
this.currentGenome.codons[5].aminoAcid = new AminoAcid('A', 'C', 'T')
}
_debugStartWithError() {
this.currentState = new ShowError(this, new CloneNucleotide(this))
this.error.innerHTML = 'test an error'
}
get currentGenome() {
return this.genomeList.last
}
showCurrent() {
if (this.currentState.id === undefined) {
return
}
const elt =
this.instructions.querySelector(`#${this.currentState.id}`)
elt.classList.add('current')
elt.scrollIntoView(true)
}
hideCurrent() {
if (this.currentState.id === undefined) {
return
}
const elt =
this.instructions.querySelector(`#${this.currentState.id}`)
elt.classList.remove('current')
}
enterState() {
this.currentState.enter()
this.showCurrent()
}
exitState() {
this.hideCurrent()
this.currentState.exit()
}
next(nextState) {
this.exitState()
this.currentState = nextState
this.enterState()
}
}
Rules.maxIterations = 10
Rules.initialGenomeBases = [
'G', 'C', 'A',
'C', 'T', 'C',
'G', 'G', 'A',
'T', 'C', 'G',
'A', 'A', 'T',
'T', 'C', 'T'
]
export default Rules