jan 2016-06-19
index.raw.html
<div id="tiles"></div>
<script src="../../js/jquery-2.2.4.min.js"></script>
<script src="memory.js"></script>
<script src="init.js"></script>
<div class="sign">jan 2016-06-19</div>
memory.coffee
init = ->
  new GameMaster().run()

class GameMaster
  constructor: (@cards = new Cards()) ->
    GameMaster.instance = @

  run: ->
    @cards.show()

    $("div.tile").addClass("pending").click ->
      checkbox = $(@).children("input")
      checkbox.prop("checked", !checkbox.prop("checked"))
      if checkbox.prop("checked")
        GameMaster.instance.checkMatching(checkbox.attr("id"))

  checkMatching: (activeCard) ->
    matching = $("input##{activeCard}:checked")
    if matching.length == 2
      @flourishFound(matching.parent())
      return

    exposed = $("div.tile.pending:has(input:checked)")
    if exposed.length > 1
      @coverUp(exposed)

  flourishFound: (tiles) ->
    tiles.unbind("click").removeClass("pending") \
      .fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100) \
      .delay(150).fadeTo(50, 0.2).promise() \
      .done(=> @checkRest())

  coverUp: (exposed) ->
    exposed.delay(1000).fadeTo(200, 0.5, ->
      $(@).children("input").prop("checked", false)).fadeTo(100, 1.0)

  checkRest: ->
    rest = $("div.tile.pending")
    if rest.length == 2
      @flourishLastPair(rest)
      $(rest).promise().done =>
        @flourishWin()

  flourishLastPair: (rest) ->
    $(rest).delay(1000).fadeOut(100, ->
      $(@).children("input").prop("checked", true)) \
      .fadeIn(100).delay(1000)
    @flourishFound(rest)

  flourishWin: ->
    $("div.tile").fadeTo(500, 1.0).promise().done ->
      $({deg: 0}).animate {deg: 1800},
        duration: 5000
        step: (now) =>
          $("div.tile").css
            transform: "rotate(#{now}deg)"

class Cards
  constructor: ->
    @backside = new Card()
    @backside.colorScheme = 'mono'
    @backside.complexity = 1

    @cards = (new Card() for [1..12])
    @cards = @cards.concat @cards
    @shuffle()

  show: ->
    area = $("div#tiles")
    backsideShape = @backside.make()
    for card in @cards
      name = "exposed#{card.id}"
      area.append """<div class="tile">
  <input type="checkbox" id="#{name}" />
  <div class="front">#{card.make()}</div>
  <div class="back">#{backsideShape}</div>
</div>"""

  shuffle: ->
    # https://coffeescript-cookbook.github.io/chapters/arrays/shuffling-array-elements
    a = @cards[..]
    i = a.length
    while --i > 0
      j = ~~(Math.random() * (i + 1)) # ~~ common optimization for Math.floor
      t = a[j]
      a[j] = a[i]
      a[i] = t
    @cards = a


class Card
  constructor: (@id) ->
    if not @id?
      while true
        @id = Math.floor(Math.random() * 2 ** 47)
        try
          new Generator(@id)
        catch
          continue
        break
    @colorScheme = 'sofa'
    @complexity = 3

  make: ->
    @g = new Generator(@id)
    "#{@shapes()}"

  shapes: -> """<svg width="100%" height="100%" viewBox="0 0 2 2"><#{@shape()} #{@style()} />""" for [1..@complexity]

  shape: ->
    @[@g.any(['rect', 'ellipse'])]()

  rect: ->
    left = @g.any([0, 1])
    top = @g.any([0, 1])
    width = @g.any([1, 2])
    height = @g.any([1, 2])
    """rect x="#{left}" y="#{top}" width="#{width}" height="#{height}" """

  ellipse: ->
    cx = @g.any([0, 1, 2])
    cy = @g.any([0, 1, 2])
    rx = @g.any([1, 2])
    ry = @g.any([1, 2])
    """ellipse cx="#{cx}" cy="#{cy}" rx="#{rx}" ry="#{ry}" """

  style: ->
    """style="fill:#{@color()};stroke:#{@color()};stroke-width:#{@length()/10};opacity:0.5" """

  color: -> @g.any(Card.colorSchemes[@colorScheme])

  length: -> @g.any([1, 2])

  @colorSchemes:
    'mono': ["#000", "#888", "#fff"]
    'sofa': ["#B22904", "#94FF19", "#FF3700", "#657CCC", "#092FB2"]

class Generator
  @isPrime: (n) ->
    for d in [2..Math.sqrt(n)+1]
      return false if n % d == 0
    return true

  @primes: n for n in [1000..2000] when @isPrime(n)

  @minId: @primes[0..2].concat(@primes[-1..]).reduce((x, y) -> x * y)

  @maxNum: @primes[0] - 1

  constructor: (@id, @index = 0)->
    throw new Error("id=#{@id} is too small, need at least #{Generator.minId}") if @id < Generator.minId

  num: (max) ->
    throw new Error("limit #{max} is too large, can do at most"
                    " #{Generator.maxNum}") if max > Generator.maxNum
    (@id % Generator.primes[@index++ % Generator.primes.length]) % max

  any: (list) ->
    list[@num(list.length)]

root = exports ? window
root.Cards = Cards
root.Card = Card
root.Generator = Generator
memory.spec.coffee
should = require "should";
{Cards, Card, Generator} = require "./memory";

describe "A Generator", ->

  it 'should return the same sequence for the same id', ->
    max = 100
    count = 10
    id = 111111111111111

    g1 = new Generator(id)
    seq1 = (g1.num(100) for [1..count])
    g2 = new Generator(id)
    seq2 = (g2.num(100) for [1..count])

    seq1.length.should.equal(count)
    "#{seq1}".should.equal("#{seq2}")

  it 'should return different sequences for different ids', ->
    max = 100
    count = 10

    g1 = new Generator(111111111111111)
    seq1 = (g1.num(100) for [1..count])
    g2 = new Generator(222222222222222)
    seq2 = (g2.num(100) for [1..count])

    seq1.length.should.equal(count)
    "#{seq1}".should.not.equal("#{seq2}")

  it 'should not allow ids that are too small', ->
    (-> new Generator(3)).should.throw(/too small/)


describe "A card", ->
  id = 111111111111111
  renderedCard = new Card(id).make()

  it "should make a div", ->
    renderedCard.should.match(/</)

  it "should resolve all parameters", ->
    renderedCard.should.not.match(/{/)

  it "should be reproducible", ->
    renderedCard.should.equal(new Card(id).make())


describe "A deck of cards", ->
  deck = new Cards()
  cardIds = (c.id for c in deck.cards)
  uniqueIds = {}
  uniqueIds[cardIds[i]] = cardIds[i] for i in [0..cardIds.length-1]
  uniqueIds = Object.keys(uniqueIds)

  it "should cointain more than one card", ->
    cardIds.length.should.be.greaterThan(1)

  it "should contain each card exactly twice", ->
    cardIds.length.should.equal(2 * uniqueIds.length)
: