rubysketch-solitaire 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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