legacy/outils/memory/Memory.js

import { j3pAddContent, j3pAddElt, j3pArrondi, j3pElement, j3pEmpty, j3pGetNewId, j3pGetRandomInt, j3pIsHtmlElement, j3pShowError, j3pShuffle } from 'src/legacy/core/functions'

import { j3pAffiche } from 'src/lib/mathquill/functions'
import 'src/legacy/outils/memory/memory.css'
import { j3pCreeSVG } from 'src/legacy/core/functionsSvg'
import { getMtgCore } from 'src/lib/outils/mathgraph'
import foncarte from './foncarte.jpg'
import foncarte2 from './foncarte2.jpg'
import { placeJouerSon } from 'src/legacy/outils/zoneStyleMathquill/functions'

const typesValides = ['string', 'mathgraph', 'image', 'son']

/**
 * @typedef Carte
 * @property {string} type string|image|mathgraph
 * @property {string} content string|image|mathgraph Pour le type string c’est le contenu à afficher (qui peut être passé directement sans fournir un objet), pour image c’est son url, et pour mathgraph le code base64 de la figure (pas encore implémenté)
 */
/**
 * @typedef CoupleCartes
 * @property {string|Carte} 0
 * @property {string|Carte} 1
 */

class Memory {
  /**
   * @param conteneur div qui contient le memory
   * @param {CoupleCartes[]} couples tableau qui contient les couples (avec éventuellement leur type si c’est une figure mg32 mais j’ai pas fait )
   * @param onComplete fonction appelée quand le memory est terminé
   * @param {object} [options]
   * @param {boolean} [options.antymemory=false] Si true les cartes sont visibles des le départ
   */
  constructor (conteneur, couples, onComplete, options = {}) {
    if (typeof conteneur === 'string') conteneur = j3pElement(conteneur)
    if (!j3pIsHtmlElement(conteneur)) throw Error('Conteneur invalide')
    /**
     * Le conteneur
     * @type {HTMLElement}
     */
    this.conteneur = conteneur

    // check de couples
    if (!Array.isArray(couples) || couples.some(couple => !Array.isArray(couple) || couple.length !== 2)) {
      throw Error('couples fournis invalides (il faut un tableau de tableaux à deux éléments)')
    }
    for (const couple of couples) {
      for (const elt of couple) {
        if (typeof elt === 'string' || typeof elt === 'number') continue
        if (typeof elt !== 'object') throw Error(`couples contient un élément invalide ${typeof elt}`)
        // if (!elt.content || typeof elt.content !== 'string') throw Error(`couples contient un élément invalide (propriété content de type ${typeof elt.content} qui vaut ${elt.content})`)
        if (!typesValides.includes(elt.type)) throw Error(`couples contient un élément invalide (propriété type qui n’est pas dans ${typesValides.join('|')}`)
      }
    }

    /**
     * Les couples de contenus.
     * @type {CoupleCartes}
     */
    this.couples = [...couples]
    this.nbCouples = this.couples.length
    this.antimemory = Boolean(options.antimemory)
    if (typeof onComplete !== 'function') throw Error('Paramètre onComplete invalide')
    this.onComplete = onComplete
    this.ok = false
    this.nbCoups = 0
    this.disabled = false
    this.score = 0
    this.okLance = false
    this.petikeblo = false
    this.clicPretListener = this.clicPret.bind(this)
  }

  _afficheCartes () {
    this.nbLignes = Math.ceil(Math.sqrt(this.nbCouples * 2))
    this.PourCompt = j3pAddElt(this.conteneur, 'div', null, {})
    const ty = (this.antimemory) ? 2 * this.nbCouples : (2 * this.nbCouples)
    const ty1 = (this.antimemory) ? ' coups (attendus: ' + ty + ' , max: ' + 2 * ty + ')' : ' coups (attendus: ' + ty + ')'
    j3pAddContent(this.PourCompt, 'Compteur: ' + this.nbCoups + ty1)
    this.PourCompt.style.color = '#0a0'
    this.plateau = j3pAddElt(this.conteneur, 'div', null, {
      style: {
        display: 'grid',
        gridTemplateColumns: 'repeat(' + this.nbLignes + ', 1fr)',
        gridTemplateRows: 'repeat(' + Math.ceil(this.nbCouples * 2 / this.nbLignes) + ', 1fr)',
        rowGap: '10px',
        columnGap: '10px',
        perspective: '1000px',
        gridAutoRows: '100px',
        userSelect: 'none'
      }
    })
    /**
     * @typedef Carte
     * @property {number} [indexCouple] L’index du couple de cartes concernées dans this.couples, affecté par _setContent
     * @property {HTMLElement} faceT
     * @property {HTMLElement} fond
     * @property {HTMLElement} face
     * @property {string} etat peut valoir cachée|bloquée|visible
     * @property {function} onClick listener du clic sur le fond (appelle _retourne)
     */
    /**
     * Les cartes du memory
     * @type {Carte[]}
     */
    this.cartes = []

    // on crée toutes les cartes, sans contenu pour le moment
    for (let index = 0; index < this.nbCouples * 2; index++) {
      const amf = (this.antimemory) ? 'memoryCarteFaceMontre2' : 'memoryCarteFaceRevient'
      const amfond = (this.antimemory) ? 'memoryCarteDosCache2' : 'memoryCarteDosRevient'
      const faceT = j3pAddElt(
        this.plateau, 'div', null, {
          className: amf,
          style: {
            gridColumn: (index % this.nbLignes) + 1,
            gridRow: Math.ceil((index + 1) / this.nbLignes),
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            textAlign: 'center',
            minHeight: '100px',
            minWidth: '100px',
            width: '100%',
            height: '100%',
            borderRadius: '10px'
          }
        })
      const fond = j3pAddElt(
        this.plateau, 'div', null, {
          className: amfond,
          style: {
            gridColumn: (index % this.nbLignes) + 1,
            gridRow: Math.ceil((index + 1) / this.nbLignes)
          }
        })
      const fonfond = (index < this.nbCouples) ? 'url(' + foncarte + ')' : 'url(' + foncarte2 + ')'
      const divSon = j3pAddElt(
        this.plateau, 'div', null, {
          style: {
            gridColumn: (index % this.nbLignes) + 1,
            gridRow: Math.ceil((index + 1) / this.nbLignes),
            borderRadius: '5px',
            border: '1px solid black',
            background: '#aeef59',
            zIndex: 100,
            width: '112px',
            height: '41px',
            paddingLeft: '5px',
            display: 'none'
          }
        })
      placeJouerSon(divSon, this.cartes[index], this, 'sonContent')
      fond.style.backgroundImage = fonfond
      const onClick = this._retourne.bind(this, index)
      const onClick2 = this._range.bind(this, index)
      const fclret = (this.antimemory) ? faceT : fond
      faceT.addEventListener('click', onClick2, false)
      fond.addEventListener('click', onClick2, false)
      fclret.addEventListener('click', onClick, false)
      this.cartes.push({
        faceT,
        fond,
        etat: 'cachée',
        onClick,
        divSon
      })
    }

    this.nbBloquees = 0
    this.nbBloquees = 0
    this.dejaRetourne = []

    // on génère une liste mélangée des index des cartes
    const ff = Array.from(this.cartes.keys())
    const ff1 = j3pShuffle(ff.slice(0, ff.length / 2))
    const ff2 = j3pShuffle(ff.slice(ff.length / 2))
    const randomIndexes = []
    for (let i = 0; i < ff1.length; i++) {
      randomIndexes.push(ff1[i], ff2[i])
    }
    // et on affecte nos contenus à chaque carte
    for (const [indexCouple, couple] of this.couples.entries()) {
      for (const cardContent of couple) {
        let content, type
        if (typeof cardContent === 'string') {
          content = cardContent
          type = 'string'
        } else {
          content = cardContent.content
          type = cardContent.type
        }
        const indexCarte = randomIndexes.pop()
        this._setContent({ content, type, indexCouple, indexCarte })
        if (!this.antimemory) {
          this.cartes[indexCarte].fond.classList.remove('memoryCarteDosRevient')
          this.cartes[indexCarte].faceT.classList.remove('memoryCarteFaceRevient')
          this.cartes[indexCarte].fond.classList.add('memoryCarteDosCache')
          this.cartes[indexCarte].faceT.classList.add('memoryCarteFaceMontre')
        }
      }
    }
    this.couplesAplace = []
  }
  // toutes les cartes sont maintenant complètes

  clicPret () {
    this._hideTout()
  }

  _setContent ({ content, indexCarte, indexCouple, type }) {
    const carte = this.cartes[indexCarte]
    switch (type) {
      case 'string':
        j3pAffiche(carte.faceT, null, content)
        carte.faceT.style.backgroundOld = ''
        break
      case 'mathgraph':
        {
          const sgvId = j3pGetNewId('mtgmemo')
          j3pCreeSVG(carte.faceT, { id: sgvId, width: 4 / 3 * content.width - 32, height: 8 / 9 * content.height + 7 })
          carte.faceT.margin = '5px'
          // carte.faceT.style.width = (4 / 3 * content.width - 32 + 10) + 'px'
          carte.faceT.style.background = '#fff'
          carte.faceT.style.backgroundOld = '#fff'
          carte.faceT.style.display = 'flex'
          this.mtgAppLecteur.addDoc(sgvId, content.txtFigure, true)
          if (content.A) this.mtgAppLecteur.giveFormula2(sgvId, 'A', content.A)
          if (content.ALEA) this.mtgAppLecteur.giveFormula2(sgvId, 'ALEA', content.ALEA)
          this.mtgAppLecteur.calculateAndDisplayAll(true)
          // this.mtgAppLecteur.display(sgvId)
        }
        break
      case 'image': {
        const immm = j3pAddElt(carte.faceT, 'img', '', { src: content.txtFigure, width: content.width, height: content.height })
        immm.style.width = content.width + 'px'
        immm.style.height = content.height + 'px'
        carte.faceT.style.background = '#fff'
        carte.faceT.style.backgroundOld = '#fff'
      }
        break
      case 'son':
        carte.divSon.style.display = ''
        carte.yaSon = true
        carte.sonContent = content.txtFigure
        carte.faceT.style.backgroundOld = ''
        break
      default:
        throw Error(`type ${type} inconnu`)
    }
    carte.indexCouple = indexCouple
  }

  _retourne (indexCarte) {
    if (this.petikeblo) return
    // Suivant le nb de cartes déjà retournées
    // si 0 ça la _retourne
    // si 1 ça la _retourne puis cache les deux 3 secondes plus tard
    // si 2 rien
    if (this.dejaRetourne.length < 2) this._show(indexCarte)
  }

  _range (indexCarte) {
    if (this.petikeblo) return
    if (this.dejaRetourne.length === 1 && this.antimemory) {
      if (this.dejaRetourne[0].indexCarte === indexCarte) {
        this._hide()
        this.petikeblo = true
        setTimeout(this._remetkeblo.bind(this), 10)
        return
      }
    }
    if (this.dejaRetourne.length === 2) {
      clearTimeout(this.funcHide)
      this._hide()
      this.petikeblo = true
      setTimeout(this._remetkeblo.bind(this), 10)
    }
  }

  _remetkeblo () {
    this.petikeblo = false
  }

  _testEgal () {
    if (this.dejaRetourne[0].indexCouple === this.dejaRetourne[1].indexCouple) {
      this.nbBloquees += 2
      for (const { indexCarte } of this.dejaRetourne) {
        const carte = this.cartes[indexCarte]
        carte.faceT.classList.remove('memoryCarteFaceMontre')
        carte.fond.classList.remove('memoryCarteDosCache')
        carte.faceT.classList.remove('memoryCarteFaceRevient')
        carte.fond.classList.remove('memoryCarteDosRevient')
        carte.faceT.classList.add('memoryCarteFaceBloquee')
        carte.fond.classList.add('memoryCarteDosBloquee')
        carte.faceT.style.pointerEvents = 'none'
        carte.faceT.style.background = '#75e579'
        carte.etat = 'bloquée'
      }
      this.dejaRetourne = []
      if (this.nbBloquees === this.nbCouples * 2) {
        this.ok = true
        if (!this.antimemory) {
          const dep = Math.max(0, this.nbCoups - this.nbCouples * 2 - 2)
          this.score = Math.max(0, 1 - dep * 0.05)
        } else {
          this.score = Math.max(0, Math.min(1, 2 - this.nbCoups / (this.nbCouples * 2) + 0.1))
        }
        this.onComplete(this.score)
      }
    } else {
      for (const { indexCarte } of this.dejaRetourne) {
        const carte = this.cartes[indexCarte]
        carte.faceT.style.background = '#e30a0a'
        carte.faceT.style.transition = 'background 1.5s'
        setTimeout(() => {
          carte.faceT.style.background = carte.faceT.style.backgroundOld
          carte.faceT.style.transition = ''
        }, 2000)
      }
      const temps = (this.antimemory) ? 2000 : 15000
      this.funcHide = setTimeout(this._hide.bind(this), temps)
      this.petikeblo = true
      setTimeout(this._remetkeblo.bind(this), 2000)
    }
  }

  /**
   * Cache les cartes retournées
   */
  _hide () {
    if (this.disabled) return
    for (const { indexCarte } of this.dejaRetourne) {
      const carte = this.cartes[indexCarte]
      const amfond = (!this.antimemory) ? 'memoryCarteDosRevient' : 'memoryCarteDosCache2'
      const avfond = (!this.antimemory) ? 'memoryCarteDosCache' : 'memoryCarteDosCache2'
      if (!this.antimemory) {
        carte.divSon.style.display = 'none'
      }
      const amf = (!this.antimemory) ? 'memoryCarteFaceRevient' : 'memoryCarteFaceMontre2'
      const avf = (!this.antimemory) ? 'memoryCarteFaceMontre' : 'memoryCarteFaceRevient2'
      carte.faceT.classList.remove(avf)
      carte.fond.classList.remove(avfond)
      carte.faceT.classList.add(amf)
      carte.fond.classList.add(amfond)
      carte.faceT.style.background = carte.faceT.style.backgroundOld
      carte.etat = 'cachée'
    }
    this.dejaRetourne = []
  }

  _hideTout () {
    this.cartes.forEach(carte => {
      if (!this.antimemory) {
        carte.divSon.style.display = 'none'
      }
      carte.faceT.classList.remove('memoryCarteFaceMontre')
      carte.fond.classList.remove('memoryCarteDosCache')
      carte.faceT.classList.add('memoryCarteFaceRevient')
      carte.fond.classList.add('memoryCarteDosRevient')
    })
  }

  _show (indexCarte) {
    if (this.disabled) return
    const carte = this.cartes[indexCarte]
    if (carte.etat === 'visible') return
    let indexCouple = carte.indexCouple
    if (carte.indexCouple === undefined) {
      j3pEmpty(carte.faceT)
      let ic, oksort
      let cmpt = 0
      do {
        cmpt++
        ic = j3pGetRandomInt(0, this.couplesAplace.length - 1)
        oksort = (cmpt < 100) && ((this.dejaRetourne.length === 0) ? false : this.couplesAplace[ic].indexCouple === this.dejaRetourne[0].indexCouple)
      } while (oksort)
      indexCouple = this.couplesAplace[ic].indexCouple
      this._setContent({ content: this.couplesAplace[ic].content, indexCarte, indexCouple, type: this.couplesAplace[ic].type })
      this.couplesAplace.splice(ic, 1)
    }
    this.nbCoups++
    j3pEmpty(this.PourCompt)
    const ty = (this.antimemory) ? 2 * this.nbCouples : (2 * this.nbCouples)
    const ty1 = (this.antimemory) ? ' coups (attendus: ' + ty + ' , max: ' + 2 * ty + ')' : ' coups (attendus: ' + ty + ')'
    j3pAddContent(this.PourCompt, 'Compteur: ' + this.nbCoups + ty1)
    if (!this.antimemory) {
      if (carte.yaSon) carte.divSon.style.display = ''
    }
    if (this.nbCoups > ty) this.PourCompt.style.color = '#F00'
    if (this.antimemory && this.nbCoups >= 2 * ty) this.onComplete()
    carte.etat = 'visible'
    this.dejaRetourne.push({ indexCouple, indexCarte })
    const avfond = (!this.antimemory) ? 'memoryCarteDosRevient' : 'memoryCarteDosCache2'
    const amfond = (!this.antimemory) ? 'memoryCarteDosCache' : 'memoryCarteDosCache2'
    const avf = (!this.antimemory) ? 'memoryCarteFaceRevient' : 'memoryCarteFaceMontre2'
    const amf = (!this.antimemory) ? 'memoryCarteFaceMontre' : 'memoryCarteFaceRevient2'
    carte.fond.classList.remove(avfond)
    carte.faceT.classList.remove(avf)
    carte.fond.classList.add(amfond)
    carte.faceT.classList.add(amf)
    if (this.antimemory) carte.faceT.style.background = '#2573b4'
    // si on avait 1, c’est passé à 2 ligne précédente, on teste si on a une paire
    if (this.dejaRetourne.length === 2) this._testEgal()
  }

  disable () {
    if (!this.okLance) {
      setTimeout(() => {
        this.disable()
      }, 10)
      return
    }
    this.disabled = true
    for (const carte of this.cartes) {
      carte.fond.removeEventListener('click', carte.onClick, false)
      if (carte.indexCouple === undefined) {
        const cp = this.couplesAplace.pop()
        this._setContent({ content: cp.content, indexCouple: cp.indexCouple, indexCarte: this.cartes.indexOf(carte), type: cp.type })
      }
      if (carte.etat !== 'bloquée') {
        carte.etat = 'cachée'
        carte.fond.classList.remove('memoryCarteDosRevient')
        carte.faceT.classList.remove('memoryCarteFaceRevient')
        carte.faceT.classList.remove('memoryCarteFaceMontre')
        carte.fond.classList.remove('memoryCarteDosCache')
        carte.fond.classList.add('memoryCarteDosCache')
        carte.faceT.classList.add('memoryCarteFaceDisable')
        carte.faceT.style.background = ''
        carte.faceT.addEventListener('click', this._showPeer.bind(this, carte.indexCouple))
      }
    }
  }

  isOk () {
    return this.ok === true
  }

  getScore () {
    return this.score
  }

  getScorePourcent () {
    return j3pArrondi(this.nbBloquees / (this.nbCouples * 2), 1)
  }

  _showPeer (indexCouple) {
    for (const carte of this.cartes) {
      if (carte.indexCouple === indexCouple) {
        if (carte.etat === 'cachée') {
          carte.faceT.style.border = '2px solid blue'
          carte.faceT.style.background = '#1bc5d9'
          carte.etat = 'visible'
        } else {
          carte.faceT.style.border = ''
          carte.faceT.style.background = ''
          carte.etat = 'cachée'
        }
      } else {
        carte.faceT.style.border = ''
        carte.faceT.style.background = ''
        carte.etat = 'cachée'
      }
    }
  }

  /**
   * Créé un Memory, l’affiche dans le DOM puis le _retourne
   * @param {HTMLElement} conteneur
   * @param {CoupleCartes[]} couples La liste des couples, chacun est un tableau de deux éléments, chaque élément peut être une string (qui sera passée à j3pAffiche) ou un objet {content, type} où type peut valoir image|mathgraph
   * @param {function} onComplete la callback qui sera appelée quand le memory sera complété
   * @return {Memory}
   */
  static create (conteneur, couples, onComplete, param) {
    const memory = new Memory(conteneur, couples, onComplete, param)
    getMtgCore({ withMathJax: true })
      .then(
        // success
        (mtgAppLecteur) => {
          memory.mtgAppLecteur = mtgAppLecteur
          memory.mtgAppLecteur.removeAllDoc()
          memory._afficheCartes()
          memory.okLance = true
        },
        // failure
        (error) => {
          j3pShowError(error, { message: 'mathgraph n’est pas correctement chargé', mustNotify: true })
        })
    // plantage dans enonceMain
      .catch(j3pShowError)
    return memory
  }
}

export default Memory