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,116 @@
1
+ using RubySketch
2
+
3
+
4
+ EASINGS = {
5
+ linear: lambda { |x| x },
6
+ sineIn: lambda { |x| 1.0 - Math.cos(x * Math::PI / 2) },
7
+ sineOut: lambda { |x| Math.sin(x * Math::PI / 2) },
8
+
9
+ quadIn: lambda { |x| quadIn x },
10
+ cubicIn: lambda { |x| cubicIn x },
11
+ quartIn: lambda { |x| quartIn x },
12
+ quintIn: lambda { |x| quintIn x },
13
+ circIn: lambda { |x| circIn x },
14
+ backIn: lambda { |x| backIn x },
15
+ expoIn: lambda { |x| expoIn x },
16
+ elasticIn: lambda { |x| elasticIn x },
17
+ bounceIn: lambda { |x| 1.0 - bounceOut(1.0 - x) },
18
+
19
+ quadOut: lambda { |x| 1.0 - quadIn(1.0 - x) },
20
+ cubicOut: lambda { |x| 1.0 - cubicIn(1.0 - x) },
21
+ quartOut: lambda { |x| 1.0 - quartIn(1.0 - x) },
22
+ quintOut: lambda { |x| 1.0 - quintIn(1.0 - x) },
23
+ circOut: lambda { |x| 1.0 - curcIn(1.0 - x) },
24
+ backOut: lambda { |x| 1.0 - backIn(1.0 - x) },
25
+ expoOut: lambda { |x| 1.0 - expoIn(1.0 - x) },
26
+ elasticOut: lambda { |x| 1.0 - elasticIn(1.0 - x) },
27
+ bounceOut: lambda { |x| bounceOut x },
28
+
29
+ sineInOut: lambda { |x| x < 0.5 ? sineIn(x) : sineOut(x) },
30
+ quadInOut: lambda { |x| x < 0.5 ? quadIn(x) : quadOut(x) },
31
+ cubicInOut: lambda { |x| x < 0.5 ? cubicIn(x) : cubicOut(x) },
32
+ quartInOut: lambda { |x| x < 0.5 ? quartIn(x) : quartOut(x) },
33
+ quintInOut: lambda { |x| x < 0.5 ? quintIn(x) : quintOut(x) },
34
+ circInOut: lambda { |x| x < 0.5 ? circIn(x) : circOut(x) },
35
+ backInOut: lambda { |x| x < 0.5 ? backIn(x) : backOut(x) },
36
+ expoInOut: lambda { |x| x < 0.5 ? expoIn(x) : expoOut(x) },
37
+ elasticInOut: lambda { |x| x < 0.5 ? elasticIn(x) : elasticOut(x) },
38
+ bounceInOut: lambda { |x| x < 0.5 ? bounceIn(x) : bounceOut(x) }
39
+ }
40
+
41
+ def quadIn(x)
42
+ x ** 2
43
+ end
44
+
45
+ def cubicIn(x)
46
+ x ** 3
47
+ end
48
+
49
+ def quartIn(x)
50
+ x ** 4
51
+ end
52
+
53
+ def quintIn(x)
54
+ x ** 5
55
+ end
56
+
57
+ def circIn(x)
58
+ 1.0 - Math.sqrt(1.0 - x ** 2)
59
+ end
60
+
61
+ def backIn(x)
62
+ 2.70158 * x ** 3 - 1.70158 * x ** 2
63
+ end
64
+
65
+ def expoIn(x)
66
+ x == 0 ? 0.0 : 2.0 ** (10.0 * x - 10.0)
67
+ end
68
+
69
+ def elasticIn(x)
70
+ c = Math::PI * 2.0 / 3.0
71
+ case x
72
+ when 0 then 0
73
+ when 1 then 1
74
+ else -(2 ** (10.0 * x - 10.0)) * Math.sin((x * 10.0 - 10.75) * c)
75
+ end
76
+ end
77
+
78
+ def bounceOut(x)
79
+ n1, d1 = 7.5625, 2.75
80
+ case
81
+ when x < 1.0 / d1 then n1 * x * x
82
+ when x < 2.0 / d1 then x -= 1.5 / d1; n1 * x * x + 0.75;
83
+ when x < 2.5 / d1 then x -= 2.25 / d1; n1 * x * x + 0.9375;
84
+ else x -= 2.625 / d1; n1 * x * x + 0.984375;
85
+ end
86
+ end
87
+
88
+
89
+ def animate(name = unique, seconds, ease: :expoOut, &block)
90
+ fun = EASINGS[ease]
91
+ start = now
92
+ eachDrawBlock = lambda do
93
+ t = (now - start) / seconds
94
+ if t >= 1.0
95
+ block.call fun.call(1.0), true, 1.0
96
+ else
97
+ block.call fun.call(t), false, t
98
+ startTimer name, 0, &eachDrawBlock
99
+ end
100
+ end
101
+ startTimer name, 0, &eachDrawBlock
102
+ end
103
+
104
+ def animateValue(name = unique, seconds, from:, to:, **kwargs, &block)
105
+ animate name, seconds, **kwargs do |t, finished, tt|
106
+ block.call lerp(from, to, t), finished, t, tt
107
+ end
108
+ end
109
+
110
+ def move(obj, toPos, seconds, **kwargs, &block)
111
+ from, to = obj.pos.dup, toPos.dup
112
+ animate seconds, **kwargs do |t, *args|
113
+ obj.pos = Vector.lerp(from, to, t)
114
+ block&.call t, *args
115
+ end
116
+ end
@@ -0,0 +1,67 @@
1
+ using RubySketch
2
+
3
+
4
+ class Button < Sprite
5
+
6
+ include CanDisable
7
+
8
+ def initialize(
9
+ label, *args, rgb: 200, width: 1, fontSize: 24, round: 5, **kwargs, &block)
10
+
11
+ super 0, 0, 44 * width, 44, *args, **kwargs, &block
12
+ @label, @rgb, @fontSize, @round = label, [rgb].flatten, fontSize, round
13
+ @click = nil
14
+ setup
15
+ end
16
+
17
+ def clicked(&block)
18
+ @click = block
19
+ nil
20
+ end
21
+
22
+ private
23
+
24
+ def setup()
25
+ pressing = false
26
+ mousePressed {pressing = true}
27
+ mouseReleased {pressing = false}
28
+
29
+ draw do
30
+ offset = 5
31
+ y = pressing ? (offset - (enabled? ? 2 : 3.5)) : 0
32
+ h = self.h - y
33
+ offset -= y
34
+ fill *@rgb.map {|n| n - 20}
35
+ rect 0, y, w, h, *@round
36
+ fill *@rgb
37
+ rect 0, y, w, h - offset, *@round
38
+ fill enabled? ? 255 : 180
39
+ textAlign CENTER, CENTER
40
+ textSize @fontSize
41
+ text @label, 0, y, w, h - offset
42
+ end
43
+
44
+ mouseClicked do
45
+ if enabled?
46
+ @click.call if @click
47
+ else
48
+ shake
49
+ end
50
+ sound.play gain: 0.5
51
+ end
52
+ end
53
+
54
+ def shake(strength = 10)
55
+ strength = strength.to_f
56
+ x = self.x
57
+ animate 0.3 do |t, finished|
58
+ self.x = x + (finished ? 0 : strength * (1.0 - t))
59
+ strength *= -1
60
+ end
61
+ end
62
+
63
+ def sound()
64
+ @sound ||= loadSound dataPath 'button.mp3'
65
+ end
66
+
67
+ end# Button
@@ -0,0 +1,103 @@
1
+ using RubySketch
2
+
3
+
4
+ class Dialog < Scene
5
+
6
+ def initialize(background: 0, alpha: 100, z: 1000)
7
+ super
8
+ @background, @alpha = background, 0
9
+ overlay.z = z
10
+ animate 0.2 do |t|
11
+ @alpha = alpha * t
12
+ end
13
+ end
14
+
15
+ def addElement(sprite)
16
+ elements.push sprite
17
+ addSprite sprite if active?
18
+ updateLayout
19
+ end
20
+
21
+ def addLabel(label, rgb: [255], fontSize: 24, align: CENTER)
22
+ bounds = textFont.textBounds label, 0, 0, fontSize
23
+ addElement Sprite.new(0, 0, bounds.w, bounds.h).tap {|sp|
24
+ sp.z = overlay.z
25
+ sp.draw do
26
+ textAlign align, CENTER
27
+ textSize fontSize
28
+ fill *rgb
29
+ text label, 0, 0, sp.w, sp.h
30
+ end
31
+ }
32
+ end
33
+
34
+ def addButton(label, *args, **kwargs, &block)
35
+ addElement Button.new(label, *args, **kwargs).tap {|b|
36
+ b.z = overlay.z
37
+ b.clicked &block
38
+ }
39
+ end
40
+
41
+ def addSpace(height)
42
+ addElement Sprite.new(0, 0, 1, height).tap {|sp|
43
+ sp.draw {}
44
+ }
45
+ end
46
+
47
+ def close()
48
+ delay {parent.remove self}
49
+ end
50
+
51
+ def sprites()
52
+ super + [overlay, *elements]
53
+ end
54
+
55
+ def elements()
56
+ @elements ||= []
57
+ end
58
+
59
+ def draw()
60
+ sprite overlay, *elements
61
+ super
62
+ end
63
+
64
+ def resized(w, h)
65
+ updateLayout
66
+ end
67
+
68
+ private
69
+
70
+ MARGIN = 10
71
+
72
+ def overlay()
73
+ @overlay ||= Sprite.new(0, 0, width, height).tap do |sp|
74
+ sp.update do
75
+ sp.w = width
76
+ sp.h = height
77
+ end
78
+ sp.draw do
79
+ fill *@background, @alpha
80
+ rect 0, 0, sp.w, sp.h
81
+ end
82
+ end
83
+ end
84
+
85
+ def cancelButton()
86
+ @cancelButton ||= Button.new('CLOSE', width: 3, fontSize: 28).tap do |sp|
87
+ sp.z = overlay.z
88
+ sp.clicked {close}
89
+ end
90
+ end
91
+
92
+ def updateLayout()
93
+ w, h = width, height
94
+ allHeight = elements.map(&:height).reduce {|a, b| a + MARGIN + b} || 0
95
+ y = (h - allHeight) / 2
96
+ elements.each do |e|
97
+ e.x = (w - e.w) / 2
98
+ e.y = y
99
+ y += e.h + MARGIN
100
+ end
101
+ end
102
+
103
+ end# Dialog
@@ -0,0 +1,94 @@
1
+ class History
2
+
3
+ include CanDisable
4
+
5
+ def initialize(undos = [], redos = [])
6
+ super()
7
+ @undos, @redos = undos, redos
8
+ @group = nil
9
+ end
10
+
11
+ def push(*actions)
12
+ return if actions.empty? || disabled?
13
+ if @group
14
+ @group.push *actions
15
+ else
16
+ @undos.push actions
17
+ @redos.clear
18
+ update
19
+ end
20
+ end
21
+
22
+ def group(&block)
23
+ @group = group = [] unless @group
24
+ block.call
25
+ ensure
26
+ if group
27
+ @group = nil
28
+ push *group
29
+ end
30
+ end
31
+
32
+ def undo(&block)
33
+ actions = @undos.pop || return
34
+ actions.reverse.each {|action| block.call action}
35
+ @redos.push actions
36
+ update
37
+ end
38
+
39
+ def redo(&block)
40
+ actions = @redos.pop || return
41
+ actions.each {|action| block.call action}
42
+ @undos.push actions
43
+ update
44
+ end
45
+
46
+ def canUndo?()
47
+ !@undos.empty?
48
+ end
49
+
50
+ def canRedo?()
51
+ !@redos.empty?
52
+ end
53
+
54
+ def updated(&block)
55
+ @updated = block
56
+ end
57
+
58
+ def to_h(&dumpObject)
59
+ {
60
+ version: 1,
61
+ undos: self.class.dump(@undos, &dumpObject),
62
+ redos: self.class.dump(@redos, &dumpObject)
63
+ }
64
+ end
65
+
66
+ def self.load(hash, &restoreObject)
67
+ undos = restore hash['undos'], &restoreObject
68
+ redos = restore hash['redos'], &restoreObject
69
+ self.new undos, redos
70
+ end
71
+
72
+ private
73
+
74
+ def update()
75
+ @updated.call if @updated
76
+ end
77
+
78
+ def self.dump(xdos, &dumpObject)
79
+ xdos.map do |actions|
80
+ actions.map do |action, *args|
81
+ [action.to_s, *args.map {|obj| dumpObject.call(obj) || obj}]
82
+ end
83
+ end
84
+ end
85
+
86
+ def self.restore(xdos, &restoreObject)
87
+ xdos.map do |actions|
88
+ actions.map do |action, *args|
89
+ [action.intern, *args.map {|obj| restoreObject.call(obj) || obj}]
90
+ end
91
+ end
92
+ end
93
+
94
+ end# History
@@ -0,0 +1,71 @@
1
+ using RubySketch
2
+
3
+
4
+ class Particle
5
+
6
+ include HasSprite
7
+
8
+ def initialize()
9
+ @sprites = []
10
+ @pool = []
11
+ end
12
+
13
+ attr_reader :sprites
14
+
15
+ def new(x, y, w, h, &block)
16
+ sp = newSprite x, y, w, h
17
+ addSprite sp
18
+ @sprites.push sp
19
+ block.call sp if block
20
+ sp
21
+ end
22
+
23
+ def delete(sprite)
24
+ @sprites.delete sprite
25
+ removeSprite sprite
26
+ @pool.push sprite
27
+ sprite
28
+ end
29
+
30
+ def draw()
31
+ drawSprite @sprites
32
+ end
33
+
34
+ private
35
+
36
+ def newSprite(x, y, w, h)
37
+ popSprite(x, y, w, h) || ParticleSprite.new(self, x, y, w, h)
38
+ end
39
+
40
+ def popSprite(x, y, w, h)
41
+ @pool.pop&.tap do |sp|
42
+ sp.frame = [x, y, w, h]
43
+ end
44
+ end
45
+
46
+ end# Particle
47
+
48
+
49
+ class ParticleSprite < Sprite
50
+
51
+ def initialize(owner, *args, rgb: [255], alpha: 255, **kwargs, &block)
52
+ @owner, @rgb, @alpha = owner, rgb, alpha
53
+ super(*args, **kwargs, &block)
54
+ draw do |&draw|
55
+ fill *@rgb, @alpha
56
+ draw.call
57
+ end
58
+ end
59
+
60
+ attr_accessor :rgb, :alpha
61
+
62
+ def frame=(frame)
63
+ self.pos = frame[0, 2]
64
+ self.size = frame[2, 2]
65
+ end
66
+
67
+ def delete()
68
+ @owner.delete self
69
+ end
70
+
71
+ end# ParticleSprite
@@ -0,0 +1,128 @@
1
+ using RubySketch
2
+
3
+
4
+ class Scene
5
+
6
+ def initialize(name = self.class.name, *scenes)
7
+ @name = name
8
+ @scenes = []
9
+ @active = self.class == RootScene
10
+ @prevSize = [width, height]
11
+ resized *@prevSize
12
+ scenes.each {|scene| add scene}
13
+ end
14
+
15
+ attr_reader :name, :parent
16
+
17
+ def add(scene)
18
+ @scenes.push scene
19
+ scene.parent = self
20
+ scene.activated if active?
21
+ scene
22
+ end
23
+
24
+ def remove(scene)
25
+ @scenes.delete scene
26
+ scene.deactivated if active?
27
+ scene.parent = nil
28
+ scene
29
+ end
30
+
31
+ def transition(to, effect = nil, *args, **kwargs)
32
+ if effect
33
+ add effect.new(to, *args, **kwargs)
34
+ else
35
+ parent.add to
36
+ parent.remove self
37
+ end
38
+ to
39
+ end
40
+
41
+ def emitParticle(x, y, w, h, sec = nil, &block)
42
+ par = particle.new x, y, w, h
43
+ start = now
44
+ par.update do
45
+ time = now - start
46
+ t = sec ? time / sec : nil
47
+ result = block&.call time, t
48
+ if result == false || (t || 1) >= 1
49
+ par.update {}
50
+ par.delete
51
+ end
52
+ end
53
+ par
54
+ end
55
+
56
+ def sprites()
57
+ particle.sprites
58
+ end
59
+
60
+ def particle()
61
+ @particle ||= Particle.new
62
+ end
63
+
64
+ def update()
65
+ size = [width, height]
66
+ if size != @prevSize
67
+ resized(*size)
68
+ @prevSize = size
69
+ end
70
+ end
71
+
72
+ def draw()
73
+ update
74
+ @scenes.each do |scene|
75
+ push {scene.draw}
76
+ end
77
+ push do
78
+ blendMode ADD
79
+ particle.draw
80
+ end
81
+ end
82
+
83
+ def resized(w, h)
84
+ end
85
+
86
+ def activated()
87
+ @active = true
88
+ @scenes.each {|scene| scene.activated}
89
+ sprites.each {|sprite| addSprite sprite}
90
+ end
91
+
92
+ def deactivated()
93
+ sprites.each {|sprite| removeSprite sprite}
94
+ @scenes.each {|scene| scene.deactivated}
95
+ @active = false
96
+ end
97
+
98
+ def active?()
99
+ @active
100
+ end
101
+
102
+ def mousePressed(x, y, button)
103
+ @scenes.each {|scene| scene.mousePressed x, y, button}
104
+ end
105
+
106
+ def mouseReleased(x, y, button)
107
+ @scenes.each {|scene| scene.mouseReleased x, y, button}
108
+ end
109
+
110
+ def mouseMoved(x, y, dx, dy)
111
+ @scenes.each {|scene| scene.mouseMoved x, y, dx, dy}
112
+ end
113
+
114
+ def mouseDragged(x, y, dx, dy)
115
+ @scenes.each {|scene| scene.mouseDragged x, y, dx, dy}
116
+ end
117
+
118
+ protected
119
+
120
+ def parent=(scene)
121
+ @parent = scene
122
+ end
123
+
124
+ end# Scene
125
+
126
+
127
+ class RootScene < Scene
128
+ end# RootScene
@@ -0,0 +1,37 @@
1
+ using RubySketch
2
+
3
+
4
+ class Score
5
+
6
+ include CanDisable
7
+
8
+ def initialize(**definitions)
9
+ super()
10
+ @defs, @value = {}, 0
11
+ define **definitions
12
+ end
13
+
14
+ attr_accessor :value
15
+
16
+ def define(**definitions)
17
+ definitions.each do |name, score|
18
+ @defs[name.intern] = score
19
+ end
20
+ end
21
+
22
+ def add(name)
23
+ return if disabled?
24
+ value = @defs[name.intern]
25
+ @value += value.to_i if value
26
+ @value = 0 if @value < 0
27
+ end
28
+
29
+ def to_h()
30
+ {score: @value}
31
+ end
32
+
33
+ def load(hash)
34
+ @value = hash['score']
35
+ end
36
+
37
+ end# Score
@@ -0,0 +1,31 @@
1
+ class Settings
2
+
3
+ def initialize(path)
4
+ @path = path or raise 'invalid path'
5
+ @hash = load path
6
+ end
7
+
8
+ def []=(key, value)
9
+ hash[key] = value
10
+ save @path
11
+ end
12
+
13
+ def [](key)
14
+ hash[key]
15
+ end
16
+
17
+ private
18
+
19
+ def hash()
20
+ @hash ||= {}
21
+ end
22
+
23
+ def save(path)
24
+ File.write path, hash.to_json
25
+ end
26
+
27
+ def load(path)
28
+ @hash = File.exist?(path) ? JSON.parse(File.read path) : {}
29
+ end
30
+
31
+ end# Settings
@@ -0,0 +1,48 @@
1
+ using RubySketch
2
+
3
+
4
+ class Shake
5
+
6
+ def initialize(length, vector)
7
+ @length, @vector = length, vector
8
+ end
9
+
10
+ def update()
11
+ @length *= 0.8 if @length
12
+ @vector *= -0.8 if @vector
13
+ v = vector
14
+ @length = @vector = nil if v && v.mag < 1
15
+ end
16
+
17
+ def vector()
18
+ return nil unless @length || @vector
19
+ v = @vector&.dup&.rotate(rand -20.0...20.0) || Vector.random2D
20
+ v *= @length if @length
21
+ v
22
+ end
23
+
24
+ end# Shake
25
+
26
+
27
+ def shake(obj, length = nil, vector: nil)
28
+ shake = Shake.new length, vector
29
+ pos = obj.pos.dup
30
+ fun = -> do
31
+ v = shake.vector
32
+ break obj.pos = pos unless v
33
+ obj.pos = pos + v
34
+ shake.update
35
+ delay {fun.call}
36
+ end
37
+ fun.call
38
+ end
39
+
40
+ def shakeScreen(length = nil, vector: nil)
41
+ $shake = Shake.new length, vector
42
+ end
43
+
44
+ def drawShake()
45
+ v = $shake&.vector
46
+ translate v.x, v.y if v
47
+ $shake&.update
48
+ end
@@ -0,0 +1,6 @@
1
+ using RubySketch
2
+
3
+
4
+ def loadSound(path)
5
+ Beeps::Sound.load path
6
+ end