rubysketch-solitaire 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.github/workflows/release-gem.yml +62 -0
  4. data/.github/workflows/test.yml +23 -0
  5. data/.github/workflows/utils.rb +56 -0
  6. data/.gitignore +11 -0
  7. data/Gemfile +5 -0
  8. data/Gemfile.lock +284 -0
  9. data/LICENSE +21 -0
  10. data/Podfile +31 -0
  11. data/Podfile.lock +59 -0
  12. data/README.md +21 -0
  13. data/Rakefile +100 -0
  14. data/RubySolitaire/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  15. data/RubySolitaire/Assets.xcassets/AppIcon.appiconset/Contents.json +98 -0
  16. data/RubySolitaire/Assets.xcassets/Contents.json +6 -0
  17. data/RubySolitaire/BridgingHeader.h +10 -0
  18. data/RubySolitaire/GameView.swift +47 -0
  19. data/RubySolitaire/Preview Content/Preview Assets.xcassets/Contents.json +6 -0
  20. data/RubySolitaire/RubySolitaireApp.swift +10 -0
  21. data/RubySolitaireTests/RubySolitaireTests.swift +28 -0
  22. data/RubySolitaireUITests/RubySolitaireUITests.swift +35 -0
  23. data/RubySolitaireUITests/RubySolitaireUITestsLaunchTests.swift +25 -0
  24. data/VERSION +1 -0
  25. data/data/button.mp3 +0 -0
  26. data/data/card.png +0 -0
  27. data/data/deal1.mp3 +0 -0
  28. data/data/deal2.mp3 +0 -0
  29. data/data/deal3.mp3 +0 -0
  30. data/data/flip.mp3 +0 -0
  31. data/data/noop.mp3 +0 -0
  32. data/lib/rubysketch/solitaire/background.rb +34 -0
  33. data/lib/rubysketch/solitaire/card.rb +256 -0
  34. data/lib/rubysketch/solitaire/common/animation.rb +116 -0
  35. data/lib/rubysketch/solitaire/common/button.rb +67 -0
  36. data/lib/rubysketch/solitaire/common/dialog.rb +103 -0
  37. data/lib/rubysketch/solitaire/common/history.rb +94 -0
  38. data/lib/rubysketch/solitaire/common/particle.rb +71 -0
  39. data/lib/rubysketch/solitaire/common/scene.rb +128 -0
  40. data/lib/rubysketch/solitaire/common/score.rb +37 -0
  41. data/lib/rubysketch/solitaire/common/settings.rb +31 -0
  42. data/lib/rubysketch/solitaire/common/shake.rb +48 -0
  43. data/lib/rubysketch/solitaire/common/sound.rb +6 -0
  44. data/lib/rubysketch/solitaire/common/timer.rb +35 -0
  45. data/lib/rubysketch/solitaire/common/transitions.rb +149 -0
  46. data/lib/rubysketch/solitaire/common/utils.rb +89 -0
  47. data/lib/rubysketch/solitaire/extension.rb +25 -0
  48. data/lib/rubysketch/solitaire/klondike.rb +676 -0
  49. data/lib/rubysketch/solitaire/places.rb +177 -0
  50. data/lib/rubysketch/solitaire/start.rb +19 -0
  51. data/lib/rubysketch/solitaire.rb +80 -0
  52. data/main.rb +1 -0
  53. data/project.yml +91 -0
  54. data/solitaire.gemspec +28 -0
  55. metadata +137 -0
@@ -0,0 +1,676 @@
1
+ # -*- coding: utf-8 -*-
2
+ using RubySketch
3
+
4
+
5
+ class Klondike < Scene
6
+
7
+ def initialize(hash = nil)
8
+ super()
9
+ hash ? load(hash) : ready
10
+ end
11
+
12
+ def sprites()
13
+ super + [*cards, *places].map(&:sprite) + interfaces
14
+ end
15
+
16
+ def draw()
17
+ sprite *places.map(&:sprite)
18
+ sprite *cards.sort {|a, b| a.z <=> b.z}.map(&:sprite)
19
+ sprite *interfaces
20
+ super
21
+ end
22
+
23
+ def resized(w, h)
24
+ super
25
+ updateLayout w, h
26
+ end
27
+
28
+ def canDrop?(card)
29
+ case card.place
30
+ when *columns then card.opened?
31
+ else card.last?
32
+ end
33
+ end
34
+
35
+ def cardClicked(card)
36
+ if card.closed? && card.place == deck
37
+ deckClicked
38
+ elsif card.opened? && place = findPlaceToGo(card)
39
+ moveCard card, place, 0.3
40
+ elsif card.opened?
41
+ shake card, vector: createVector(5, 0)
42
+ noopSound.play gain: 0.5
43
+ end
44
+ end
45
+
46
+ def deckClicked()
47
+ deck.empty? ? refillDeck : drawNexts
48
+ end
49
+
50
+ def nextsClicked()
51
+ drawNexts if nexts.empty?
52
+ end
53
+
54
+ def cardDropped(x, y, card, prevPlace)
55
+ if place = getPlaceAccepts(x, y, card)
56
+ moveCard card, place, 0.2
57
+ elsif prevPlace
58
+ history.disable do
59
+ prevPos, prevTime = card.pos.xy, now
60
+ moveCard card, prevPlace, 0.15, add: false, ease: :quadIn do |t, finished|
61
+ pos, time = card.pos.xy, now
62
+ vel = (pos - prevPos) / (time - prevTime)
63
+ prevPos, prevTime = pos, time
64
+ backToPlace card, vel if finished
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def save()
71
+ settings['state'] = {
72
+ version: 1,
73
+ drawCount: nexts.drawCount,
74
+ history: history.to_h {|o| o.id if o.respond_to? :id},
75
+ score: score.to_h,
76
+ elapsedTime: elapsedTime,
77
+ moveCount: @moveCount,
78
+ places: places.map {|place| [place.name, place.cards.map(&:id)]}.to_h,
79
+ openeds: cards.select {|card| card.opened?}.map(&:id)
80
+ }
81
+ end
82
+
83
+ def load(hash)
84
+ raise 'Unknown state version' unless hash['version'] == 1
85
+
86
+ all = places + cards
87
+ findAll = -> id { all.find {|obj| obj .id == id} or raise "No object '#{id}'"}
88
+ findCard = -> id {cards.find {|card| card.id == id} or raise "No card '#{id}'"}
89
+
90
+ nexts.drawCount = hash['drawCount']
91
+
92
+ self.history = History.load hash['history'] do |id|
93
+ (id.respond_to?('=~') && id =~ /^id:/) ? findAll[id] : nil
94
+ end
95
+
96
+ self.score.load hash['score']
97
+ @elapsedTime = hash['elapsedTime']
98
+ @moveCount = hash['moveCount']
99
+
100
+ places.each do |place|
101
+ place.clear
102
+ ids = hash['places'][place.name.to_s] or raise "No place '#{place.name}'"
103
+ place.add *ids.map {|id| findCard[id]}
104
+ end
105
+
106
+ hash['openeds'].each do |id|
107
+ findCard[id].open
108
+ end
109
+
110
+ raise "Failed to restore state" unless
111
+ places.reduce([]) {|a, place| a + place.cards}.size == 52
112
+
113
+ resume
114
+ end
115
+
116
+ def inspect()
117
+ "#<Klondike:#{object_id}>"
118
+ end
119
+
120
+ private
121
+
122
+ def history()
123
+ self.history = History.new unless @history
124
+ @history
125
+ end
126
+
127
+ def history=(history)
128
+ @history = history.tap do |h|
129
+ h.updated {save}
130
+ end
131
+ end
132
+
133
+ def elapsedTime()
134
+ @elapsedTime ||= 0
135
+ if @prevTime
136
+ now_ = now
137
+ @elapsedTime += now_ - @prevTime
138
+ @prevTime = now_
139
+ end
140
+ @elapsedTime
141
+ end
142
+
143
+ def score()
144
+ @score ||= Score.new **{
145
+ openCard: 5,
146
+ moveToColumn: 5,
147
+ moveToMark: 10,
148
+ backToColumn: -15,
149
+ refillDeckOnDraw3: -20,
150
+ refillDeckOnDraw1: -100
151
+ }
152
+ end
153
+
154
+ def bestTime()
155
+ settings['bestTime'] || 24 * 60 * 60 - 1
156
+ end
157
+
158
+ def bestScore()
159
+ settings['bestScore'] || 0
160
+ end
161
+
162
+ def updateBests()
163
+ newTime = elapsedTime < bestTime
164
+ newScore = score.value > bestScore
165
+
166
+ settings['bestTime'] = elapsedTime if newTime
167
+ settings['bestScore'] = score.value if newScore
168
+
169
+ return newTime, newScore
170
+ end
171
+
172
+ def cards()
173
+ @cards ||= Card::MARKS.product((1..13).to_a)
174
+ .map {|m, n| Card.new self, m, n}
175
+ .each {|card| card.sprite.mouseClicked {cardClicked card}}
176
+ end
177
+
178
+ def places()
179
+ @places ||= [deck, nexts, *marks, *columns]
180
+ end
181
+
182
+ def deck()
183
+ @deck ||= CardPlace.new(:deck).tap do |deck|
184
+ deck.sprite.mouseClicked {deckClicked}
185
+ end
186
+ end
187
+
188
+ def nexts()
189
+ @nexts ||= NextsPlace.new(:nexts).tap do |nexts|
190
+ nexts.sprite.mouseClicked {nextsClicked}
191
+ end
192
+ end
193
+
194
+ def marks()
195
+ @marks ||= Card::MARKS.size.times.map {|i| MarkPlace.new "mark_#{i + 1}"}
196
+ end
197
+
198
+ def columns()
199
+ @culumns ||= 7.times.map.with_index {|i| ColumnPlace.new "column_#{i + 1}"}
200
+ end
201
+
202
+ def dealSound()
203
+ @dealSounds ||= %w[deal1 deal2 deal3]
204
+ .map {|s| dataPath "#{s}.mp3"}
205
+ .map {|path| loadSound path}
206
+ @dealSounds.sample
207
+ end
208
+
209
+ def flipSound()
210
+ @flipSound ||= loadSound dataPath 'flip.mp3'
211
+ end
212
+
213
+ def noopSound()
214
+ noopSound ||= loadSound dataPath 'noop.mp3'
215
+ end
216
+
217
+ def interfaces()
218
+ [undoButton, redoButton, menuButton, finishButton, status, debugButton]
219
+ end
220
+
221
+ def undoButton()
222
+ @undoButton ||= Button.new(
223
+ '◀', rgb: [120, 140, 160], fontSize: 28, round: [20, 4, 4, 20]
224
+ ).tap do |b|
225
+ b.update {b.enable history.canUndo?}
226
+ b.clicked {history.undo {|action| undo action}}
227
+ end
228
+ end
229
+
230
+ def redoButton()
231
+ @redoButton ||= Button.new(
232
+ '▶', rgb: [160, 140, 120], fontSize: 28, round: [4, 20, 20, 4]
233
+ ).tap do |b|
234
+ b.update {b.enable history.canRedo?}
235
+ b.clicked {history.redo {|action| self.redo action}}
236
+ end
237
+ end
238
+
239
+ def menuButton()
240
+ @menuButton ||= Button.new(
241
+ '≡', rgb: [140, 160, 120], fontSize: 36
242
+ ).tap do |b|
243
+ b.clicked {showMenuDialog}
244
+ end
245
+ end
246
+
247
+ def status()
248
+ @status ||= Sprite.new.tap do |sp|
249
+ sp.draw do
250
+ push do
251
+ fill 0, 20
252
+ rect 0, 0, sp.w, sp.h, 10
253
+ fill 255
254
+
255
+ mx, my, x, w = 8, 4, 0, sp.w / 3
256
+ {
257
+ Time: timeToText(elapsedTime),
258
+ Score: score.value,
259
+ Move: @moveCount || 0
260
+ }.each do |label, value|
261
+ textSize 12
262
+ textAlign LEFT, TOP
263
+ text label, x + mx, my, w - mx, sp.h - my * 2
264
+ textSize 20
265
+ textAlign LEFT, BOTTOM
266
+ text value, x + mx, my, w - mx, sp.h - my * 2
267
+ x += w
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ def finishButton()
275
+ @finishButton ||= Button.new(
276
+ 'FINISH!', rgb: [100, 200, 150], fontSize: 28, width: 5
277
+ ).tap do |b|
278
+ b.hide
279
+ b.clicked {finish!}
280
+ end
281
+ end
282
+
283
+ def debugButton()
284
+ @debugButton ||= Button.new(:DEBUG, width: 3).tap do |b|
285
+ b.hide
286
+ b.clicked {}
287
+ end
288
+ end
289
+
290
+ def showReadyDialog()
291
+ add Dialog.new(alpha: 50).tap {|d|
292
+ d.addButton 'EASY', width: 5 do
293
+ start 1
294
+ d.close
295
+ end
296
+ d.addButton 'HARD', width: 5 do
297
+ start 3
298
+ d.close
299
+ end
300
+ }
301
+ end
302
+
303
+ def showMenuDialog()
304
+ pause
305
+ add Dialog.new.tap {|d|
306
+ d.addButton 'RESUME', width: 5 do
307
+ d.close
308
+ resume
309
+ end
310
+ d.addButton 'NEW GAME', width: 5 do
311
+ startNewGame
312
+ end
313
+ d.addSpace 50
314
+ d.addLabel "Best Time: #{timeToText bestTime}"
315
+ d.addLabel "Best Score: #{bestScore}"
316
+ }
317
+ end
318
+
319
+ def showCompletedDialog(newBestTime = false, newBestScore = false)
320
+ pause
321
+ add Dialog.new.tap {|d|
322
+ d.addLabel 'Congratulations!', fontSize: 44
323
+ d.addLabel(
324
+ "Time: #{timeToText elapsedTime} #{newBestTime ? '(NEW!)' : ''}",
325
+ fontSize: 28)
326
+ d.addLabel(
327
+ "Score: #{score.value} #{newBestScore ? '(NEW!)' : ''}",
328
+ fontSize: 28)
329
+ d.addSpace 50
330
+ d.addButton 'NEW GAME', width: 5 do
331
+ startNewGame
332
+ end
333
+ }
334
+ end
335
+
336
+ def updateLayout(w, h)
337
+ card = cards.first
338
+ cw, ch = card.then {|c| [c.w, c.h]}
339
+ mx, my = Card.margin, cw * 0.2 # margin x, y
340
+ y = my
341
+
342
+ undoButton.pos = [mx, y]
343
+ redoButton.pos = [undoButton.x + undoButton.w + 2, y]
344
+ menuButton.pos = [width - (menuButton.w + mx), y]
345
+ status.pos = [redoButton.right + mx, y]
346
+ status.right = menuButton.left - mx
347
+ status.height = menuButton.h
348
+
349
+ y = undoButton.y + undoButton.h + my
350
+
351
+ deck.pos = [w - (deck.w + mx), y]
352
+ nexts.pos = [deck.x - (nexts.w + mx), deck.y]
353
+ marks.each.with_index do |mark, index|
354
+ mark.pos = [mx + (mark.w + mx) * index, deck.y]
355
+ end
356
+
357
+ y = deck.y + deck.h + my
358
+
359
+ columns.each.with_index do |column, index|
360
+ s = columns.size
361
+ m = (w - cw * s) / (s + 1) # margin
362
+ column.pos = [m + (cw + m) * index, y]
363
+ end
364
+
365
+ debugButton.pos = [mx, height - (debugButton.h + my)]
366
+ end
367
+
368
+ def ready()
369
+ elements = showReadyDialog.elements
370
+ elements.each &:hide
371
+
372
+ history.disable
373
+ deck.add *cards.shuffle
374
+ startTimer 0.5 do
375
+ placeToColumns do
376
+ history.enable
377
+ elements.each &:show
378
+ end
379
+ end
380
+ end
381
+
382
+ def start(drawCount = 1)
383
+ nexts.drawCount = drawCount
384
+
385
+ history.disable
386
+ lasts = columns.map(&:last).compact
387
+ lasts.each.with_index do |card, n|
388
+ startTimer 0.02 * n do
389
+ openCard card, gain: 0.2
390
+ if lasts.all? {|card| card.opened?}
391
+ drawNexts
392
+ history.enable
393
+ resume
394
+ end
395
+ end
396
+ end
397
+ end
398
+
399
+ def placeToColumns(&block)
400
+ firstDistribution.then do |positions|
401
+ positions.each.with_index do |(col, row), index|
402
+ startTimer index / 25.0 do
403
+ flipSound.play gain: 0.1
404
+ moveCard deck.last, columns[col], 0.5, hover: false do |t, finished|
405
+ block&.call if finished && [col, row] == positions.last
406
+ end
407
+ end
408
+ end
409
+ end
410
+ end
411
+
412
+ def firstDistribution()
413
+ n = columns.size
414
+ (0...n).map { |row| (row...n).map { |col| [col, row] } }.flatten(1)
415
+ end
416
+
417
+ def openCard(card, gain: 0.5)
418
+ return if card.opened?
419
+ history.group do
420
+ card.open 0.3
421
+ history.push [:open, card]
422
+ addScore :openCard if columns.include?(card.place)
423
+ end
424
+ flipSound.play gain: gain
425
+ end
426
+
427
+ def closeCard(card)
428
+ return if card.closed?
429
+ card.close 0.3
430
+ history.push [:close, card]
431
+ end
432
+
433
+ def moveCard(
434
+ card, toPlace, seconds = 0,
435
+ from: card.place, add: true, count: true, hover: true,
436
+ **kwargs, &block)
437
+
438
+ pos = toPlace.posFor card
439
+ card.hover base: pos.z if hover
440
+ toPlace.add card, updatePos: false if add
441
+ move card, pos, seconds, **kwargs do |t, finished|
442
+ block.call t, finished if block
443
+ cardMoved from if finished
444
+ end
445
+
446
+ dealSound.play
447
+
448
+ @moveCount ||= 0
449
+ @moveCount += 1 if count && history.enabled?
450
+
451
+ history.group do
452
+ history.push [:move, card, from, toPlace]
453
+ history.push [:moveCount, @moveCount]
454
+
455
+ fromNexts = from == nexts
456
+ fromColumn = columns.include? from
457
+ toColumn = columns.include? toPlace
458
+ fromMark = marks.include? from
459
+ toMark = marks.include? toPlace
460
+ addScore :moveToColumn if fromNexts && toColumn
461
+ addScore :backToColumn if fromMark && toColumn
462
+ addScore :moveToMark if !fromMark && toMark
463
+
464
+ openCard from.last if fromColumn && from.last&.closed?
465
+ end
466
+ end
467
+
468
+ def cardMoved(from)
469
+ openCard from.last if columns.include?(from) && from.last&.closed?
470
+ showFinishButton if finishButton.hidden? && canFinish?
471
+ completed if completed?
472
+ end
473
+
474
+ def canFinish?()
475
+ deck.empty? && nexts.empty? &&
476
+ columns.any? {|col| !col.empty?} &&
477
+ columns.all? {|col| col.each_cons(2).all? {|a, b| a.number > b.number}}
478
+ end
479
+
480
+ def completed?()
481
+ deck.empty? && nexts.empty? && columns.all?(&:empty?)
482
+ end
483
+
484
+ def drawNexts()
485
+ return if deck.empty?
486
+ history.group do
487
+ cards = nexts.drawCount.times.map {deck.pop}.compact
488
+ nexts.add *cards, updatePos: false
489
+ cards.each.with_index do |card, index|
490
+ openCard card
491
+ moveCard card, nexts, 0.3, from: deck, add: false, count: index == 0
492
+ end
493
+ end
494
+ end
495
+
496
+ def refillDeck()
497
+ history.group do
498
+ nexts.cards.reverse.each.with_index do |card, index|
499
+ closeCard card
500
+ moveCard card, deck, 0.3, count: index == 0
501
+ end
502
+ incrementRefillCount
503
+ end
504
+ #startTimer(0.4) {drawNexts}
505
+ end
506
+
507
+ def incrementRefillCount()
508
+ @refillCount ||= 0
509
+ @refillCount += 1
510
+ case nexts.drawCount
511
+ when 1 then addScore :refillDeckOnDraw1
512
+ when 3 then addScore :refillDeckOnDraw3 if @refillCount >= 3
513
+ end
514
+ end
515
+
516
+ def getPlaceAccepts(x, y, card)
517
+ (columns + marks).find {|place| place.accept? x, y, card}
518
+ end
519
+
520
+ def findPlaceToGo(card)
521
+ return nil if marks.include?(card.place)
522
+ marks.find {|place| place.accept? *place.center.to_a(2), card} ||
523
+ columns.shuffle.find {|place| place.accept? *place.center.to_a(2), card}
524
+ end
525
+
526
+ def showFinishButton()
527
+ finishButton.tap do |b|
528
+ m = Card.margin
529
+ b.x = marks.last.then {|mark| mark.x + mark.w} + m * 2
530
+ b.y = -deck.h
531
+ b.w = width - b.x - m
532
+ b.h = deck.h
533
+ end
534
+ pos = finishButton.pos.dup
535
+ pos.y = deck.y
536
+ move finishButton.show, pos, 1, ease: :bounceOut
537
+ end
538
+
539
+ def finish!(cards = columns.map(&:cards).flatten.sort)
540
+ card = cards.shift or return
541
+ place = marks.find {|mark| mark.accept? mark.x, mark.y, card} or return
542
+ moveCard card, place, 0.3
543
+ startTimer(0.05) {finish! cards}
544
+ end
545
+
546
+ def completed()
547
+ return if @completed
548
+ @completed = true
549
+
550
+ history.disable
551
+ showCompletedDialog *updateBests
552
+
553
+ gravity 0, 1000
554
+ ground = createSprite(0, height + cards.first.height + 5, width, 10).tap do |sp|
555
+ sp.dynamic = false
556
+ end
557
+
558
+ cards.group_by(&:number).values
559
+ .reverse
560
+ .map(&:shuffle)
561
+ .flatten
562
+ .each.with_index do |card, index|
563
+
564
+ startTimer 0.1 * index do
565
+ card.place&.pop
566
+ card.sprite.tap do |sp|
567
+ sp.fixAngle
568
+ sp.contact? {|o| o == ground}
569
+ bounce = 0
570
+ sp.contact {|_, action|
571
+ next unless action == :begin
572
+ bounce += 1
573
+ if bounce > 3
574
+ sp.dynamic = false
575
+ sp.hide
576
+ else
577
+ vec = Vector.fromAngle(rand -135..-45) * rand(75..100)
578
+ emitDust sp.center, vec, size: 10..20
579
+ end
580
+ }
581
+ sp.dynamic = true
582
+ sp.restitution = 0.5
583
+ sp.vx = rand -20..100
584
+ sp.vy = -300
585
+ end
586
+ end
587
+ end
588
+ end
589
+
590
+ def backToPlace(card, vel)
591
+ vec = vel.dup.normalize * sqrt(vel.mag) / 10 * sqrt(card.count)
592
+ return if vec.mag < 3
593
+ shakeScreen vector: vec
594
+ emitDustOnEdges card, size: sqrt(vec.mag).then {|m| m..(m * 5)}
595
+ end
596
+
597
+ def emitDustOnEdges(card, amount = 10, speed: 10, **kwargs)
598
+ amount.times {
599
+ pos = createVector *randomEdge(card)
600
+ vec = (pos - card.center).normalize * speed
601
+ emitDust pos, vec, **kwargs
602
+ }
603
+ end
604
+
605
+ def emitDust(pos, vec, sec = 0.5, size: 2.0..10.0)
606
+ size_ = rand size
607
+ par = emitParticle pos.x, pos.y, size_, size_, sec
608
+ animateValue(sec, from: pos, to: pos + vec) {|p| par.pos = p}
609
+ animateValue(sec, from: 255, to: 0) {|a| par.alpha = a}
610
+ end
611
+
612
+ def randomEdge(card)
613
+ if rand < card.w / (card.w + card.h)
614
+ [
615
+ card.x + rand(card.w),
616
+ card.y + (rand < 0.5 ? 0 : card.h)
617
+ ]
618
+ else
619
+ [
620
+ card.x + (rand < 0.5 ? 0 : card.w),
621
+ card.y + rand(card.h)
622
+ ]
623
+ end
624
+ end
625
+
626
+ def timeToText(time)
627
+ Time.at(time).strftime('%M:%S')
628
+ end
629
+
630
+ def pause()
631
+ @prevTime = nil
632
+ stopTimer :save
633
+ end
634
+
635
+ def resume()
636
+ @prevTime = now
637
+ startInterval :save, 1, now: true do
638
+ save
639
+ end
640
+ end
641
+
642
+ def addScore(name)
643
+ old = score.value
644
+ score.add name if history.enabled?
645
+ history.push [:score, score.value, old] if score.value != old
646
+ end
647
+
648
+ def undo(action)
649
+ history.disable do
650
+ case action
651
+ in [:open, card] then closeCard card
652
+ in [:close, card] then openCard card
653
+ in [:move, card, from, to] then moveCard card, from, 0.2, from: to
654
+ in [:score, value, old] then score.value = old
655
+ in [:moveCount, value] then @moveCount = value - 1
656
+ end
657
+ end
658
+ end
659
+
660
+ def redo(action)
661
+ history.disable do
662
+ case action
663
+ in [:open, card] then openCard card
664
+ in [:close, card] then closeCard card
665
+ in [:move, card, from, to] then moveCard card, to, 0.2, from: from
666
+ in [:score, value, old] then score.value = value
667
+ in [:moveCount, value] then @moveCount = value
668
+ end
669
+ end
670
+ end
671
+
672
+ def startNewGame()
673
+ transition self.class.new, [Fade, Curtain, Pixelate].sample
674
+ end
675
+
676
+ end# Klondike