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,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