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.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.github/workflows/release-gem.yml +62 -0
- data/.github/workflows/test.yml +23 -0
- data/.github/workflows/utils.rb +56 -0
- data/.gitignore +11 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +21 -0
- data/Podfile +31 -0
- data/Podfile.lock +59 -0
- data/README.md +21 -0
- data/Rakefile +100 -0
- data/RubySolitaire/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- data/RubySolitaire/Assets.xcassets/AppIcon.appiconset/Contents.json +98 -0
- data/RubySolitaire/Assets.xcassets/Contents.json +6 -0
- data/RubySolitaire/BridgingHeader.h +10 -0
- data/RubySolitaire/GameView.swift +47 -0
- data/RubySolitaire/Preview Content/Preview Assets.xcassets/Contents.json +6 -0
- data/RubySolitaire/RubySolitaireApp.swift +10 -0
- data/RubySolitaireTests/RubySolitaireTests.swift +28 -0
- data/RubySolitaireUITests/RubySolitaireUITests.swift +35 -0
- data/RubySolitaireUITests/RubySolitaireUITestsLaunchTests.swift +25 -0
- data/VERSION +1 -0
- data/data/button.mp3 +0 -0
- data/data/card.png +0 -0
- data/data/deal1.mp3 +0 -0
- data/data/deal2.mp3 +0 -0
- data/data/deal3.mp3 +0 -0
- data/data/flip.mp3 +0 -0
- data/data/noop.mp3 +0 -0
- data/lib/rubysketch/solitaire/background.rb +34 -0
- data/lib/rubysketch/solitaire/card.rb +256 -0
- data/lib/rubysketch/solitaire/common/animation.rb +116 -0
- data/lib/rubysketch/solitaire/common/button.rb +67 -0
- data/lib/rubysketch/solitaire/common/dialog.rb +103 -0
- data/lib/rubysketch/solitaire/common/history.rb +94 -0
- data/lib/rubysketch/solitaire/common/particle.rb +71 -0
- data/lib/rubysketch/solitaire/common/scene.rb +128 -0
- data/lib/rubysketch/solitaire/common/score.rb +37 -0
- data/lib/rubysketch/solitaire/common/settings.rb +31 -0
- data/lib/rubysketch/solitaire/common/shake.rb +48 -0
- data/lib/rubysketch/solitaire/common/sound.rb +6 -0
- data/lib/rubysketch/solitaire/common/timer.rb +35 -0
- data/lib/rubysketch/solitaire/common/transitions.rb +149 -0
- data/lib/rubysketch/solitaire/common/utils.rb +89 -0
- data/lib/rubysketch/solitaire/extension.rb +25 -0
- data/lib/rubysketch/solitaire/klondike.rb +676 -0
- data/lib/rubysketch/solitaire/places.rb +177 -0
- data/lib/rubysketch/solitaire/start.rb +19 -0
- data/lib/rubysketch/solitaire.rb +80 -0
- data/main.rb +1 -0
- data/project.yml +91 -0
- data/solitaire.gemspec +28 -0
- 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
|