cryptopunks 1.1.0 → 1.2.2

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.
@@ -0,0 +1,162 @@
1
+
2
+ ROBOT_COLORS = [
3
+ '000000', # color 1 - BLACK
4
+ '535353', # color 2 - BASE 2 (DARKER)
5
+ 'A4A4A4', # color 3 - BASE 1
6
+ 'A9F7FF', # color 4 - BASE 3 (LIGHTER) - eyes
7
+ ]
8
+
9
+ VAMPIRE_COLORS = [
10
+ '000000', # color 1 - BLACK
11
+ '131313', # color 2 - BASE 4 (DARKEST)
12
+ '535353', # color 3 - BASE 3 (DARKERER) - eyes
13
+ 'A4A4A4', # color 4 - BASE 2 (DARKER) - eyes
14
+ 'E0E0E0', # color 5 - BASE 1
15
+ 'F6000B', # color 6 - BASE 5 - teeth - red
16
+ ]
17
+
18
+ MUMMY_COLORS = [
19
+ '000000', # color 1 - BLACK
20
+ '1F1A15', # color 2 - BASE 5 (DARKEST)
21
+ '2A231C', # color 3 - BASE 4 (DARKERERER)
22
+ '5F5147', # color 4 - BASE 3 (DARKERER)
23
+ '927B6A', # color 5 - BASE 2 (DARKER)
24
+ 'D9B599', # color 6 - BASE 1
25
+ 'F6000B', # color 7 - BASE 6 - eyes - red
26
+ ]
27
+
28
+ ORC_COLORS = [
29
+ '000000', # color 1 - BLACK
30
+ '171a08', # color 2 - BASE 3 (DARKEST)
31
+ '333F0C', # color 3 - BASE 2 (DARKER)
32
+ '50650E', # color 4 - BASE 1
33
+ 'FFFFFF', # color 5 - BASE 4 - white
34
+ ]
35
+
36
+ SKELETON_COLORS = [
37
+ '000000', # color 1 - BLACK
38
+ 'e0e0e0', # color 2 - BASE 1
39
+ ]
40
+
41
+ DEMON_COLORS = [
42
+ '000000', # color 1 - BLACK
43
+ '390102', # color 2 - BASE 3 (DARKEST) - eyes
44
+ '630006', # color 3 - BASE 2 (DARKER) - eyes
45
+ '850008', # color 4 - BASE 1
46
+ ]
47
+
48
+ ZOMBIE_COLORS = [
49
+ '000000', # color 1 - BLACK
50
+ '5e7253', # color 2 - BASE 2 (DARKER)
51
+ '7da269', # color 3 - BASE 1
52
+ 'ff0000', # color 4 - BASE 3 - red eye
53
+ '9bbc88', # color 5 - BASE 4 (LIGHTER)
54
+ '4a010d', # color 6 - BASE 5 - mouth (female only)
55
+ ]
56
+
57
+ APE_COLORS = [
58
+ '000000', # color 1 - BLACK
59
+ '352410', # color 2 - BASE 3 (DARKEST)
60
+ '6a563f', # color 3 - BASE 2 (DARKER) - eyes
61
+ '856f56', # color 4 - BASE 1
62
+ 'a98c6b', # color 5 - BASE 4 (LIGHTER) - eyes
63
+ ]
64
+
65
+ ALIEN_COLORS = [
66
+ '000000', # color 1 - BLACK
67
+ '75bdbd', # color 2 - BASE 3 (DARKEST)
68
+ '9be0e0', # color 3 - BASE 2 (DARKER)
69
+ 'c8fbfb', # color 4 - BASE 1
70
+ ]
71
+
72
+
73
+ HUMAN_LIGHTER_BASE1 = 'ead9d9' # rgb(234 217 217) - hsl( 0° 29% 88%)
74
+ HUMAN_LIGHTER_BASE2 = 'c9b2b2' # rgb(201 178 178) - hsl( 0° 18% 74%) - eyes
75
+ HUMAN_LIGHTER_BASE3 = 'a58d8d' # rgb(165 141 141) - hsl( 0° 12% 60%) - eyes
76
+ HUMAN_LIGHTER_BASE4 = 'ffffff' # rgb(255 255 255) - hsl( 0° 0% 100%) -- white
77
+ HUMAN_LIGHTER_BASE5 = '711010' # rgb(113 16 16) - hsl( 0° 75% 25%) - mouth
78
+
79
+ HUMAN_LIGHT_BASE1 = 'dbb180' # rgb(219 177 128) - hsl( 32° 56% 68%)
80
+ HUMAN_LIGHT_BASE2 = 'd29d60' # rgb(210 157 96) - hsl( 32° 56% 60%) - eyes
81
+ HUMAN_LIGHT_BASE3 = 'a66e2c' # rgb(166 110 44) - hsl( 32° 58% 41%) - eyes
82
+ HUMAN_LIGHT_BASE4 = 'e7cba9' # rgb(231 203 169) - hsl( 33° 56% 78%)
83
+ HUMAN_LIGHT_BASE5 = '711010' # rgb(113 16 16) - hsl( 0° 75% 25%) - mouth
84
+
85
+ HUMAN_DARK_BASE1 = 'ae8b61' # rgb(174 139 97) - hsl( 33° 32% 53%)
86
+ HUMAN_DARK_BASE2 = 'a77c47' # rgb(167 124 71) - hsl( 33° 40% 47%) - eyes
87
+ HUMAN_DARK_BASE3 = '86581e' # rgb(134 88 30) - hsl( 33° 63% 32%) - eyes
88
+ HUMAN_DARK_BASE4 = 'b69f82' # rgb(182 159 130) - hsl( 33° 26% 61%)
89
+ HUMAN_DARK_BASE5 = '5f1d09' # rgb( 95 29 9) - hsl( 14° 83% 20%) - mouth
90
+
91
+ HUMAN_DARKER_BASE1 = '713f1d' # rgb(113 63 29) - hsl( 24° 59% 28%)
92
+ HUMAN_DARKER_BASE2 = '723709' # rgb(114 55 9) - hsl( 26° 85% 24%) - eyes
93
+ HUMAN_DARKER_BASE3 = '562600' # rgb( 86 38 0) - hsl( 27° 100% 17%) - eyes
94
+ HUMAN_DARKER_BASE4 = '8b532c' # rgb(139 83 44) - hsl( 25° 52% 36%)
95
+ HUMAN_DARKER_BASE5 = '4a1201' # rgb( 74 18 1) - hsl( 14° 97% 15%) - mouth
96
+
97
+
98
+ HUMAN_COLORS_LIGHT = [ ## todo/check: change to HUMAN_LIGHT_COLORS???
99
+ '000000', # color 1 - BLACK
100
+ HUMAN_LIGHT_BASE3, # color 2 - BASE 3 (DARKEST) - eyes
101
+ HUMAN_LIGHT_BASE2, # color 3 - BASE 2 (DARKER) - eyes
102
+ HUMAN_LIGHT_BASE1, # color 4 - BASE 1
103
+ HUMAN_LIGHT_BASE4, # color 5 - BASE 4
104
+ HUMAN_LIGHT_BASE5, # color 6 - BASE 5 - mouth (femaly only)
105
+ ]
106
+
107
+ HUMAN_COLORS_LIGHTER = [ ## todo/check: change to HUMAN_LIGHTER_COLORS?? or add alias - why? why not?
108
+ '000000', # color 1 - BLACK
109
+ HUMAN_LIGHTER_BASE3,
110
+ HUMAN_LIGHTER_BASE2,
111
+ HUMAN_LIGHTER_BASE1,
112
+ HUMAN_LIGHTER_BASE4,
113
+ HUMAN_LIGHTER_BASE5,
114
+ ]
115
+
116
+ HUMAN_COLORS_DARK = [
117
+ '000000', # color 1 - BLACK
118
+ HUMAN_DARK_BASE3,
119
+ HUMAN_DARK_BASE2,
120
+ HUMAN_DARK_BASE1,
121
+ HUMAN_DARK_BASE4,
122
+ HUMAN_DARK_BASE5,
123
+ ]
124
+
125
+ HUMAN_COLORS_DARKER = [
126
+ '000000', # color 1 - BLACK
127
+ HUMAN_DARKER_BASE3,
128
+ HUMAN_DARKER_BASE2,
129
+ HUMAN_DARKER_BASE1,
130
+ HUMAN_DARKER_BASE4,
131
+ HUMAN_DARKER_BASE5,
132
+ ]
133
+
134
+
135
+ HUMAN_COLORS = {
136
+ 'light' => HUMAN_COLORS_LIGHT,
137
+ 'lighter' => HUMAN_COLORS_LIGHTER,
138
+ 'dark' => HUMAN_COLORS_DARK,
139
+ 'darker' => HUMAN_COLORS_DARKER,
140
+ }
141
+
142
+
143
+
144
+ ### todo/check:
145
+ ## use a different name other than the "generic" COLORS - why? why not?
146
+
147
+ COLORS = {
148
+ ## original series
149
+ 'human' => HUMAN_COLORS,
150
+ 'zombie' => ZOMBIE_COLORS,
151
+ 'ape' => APE_COLORS,
152
+ 'alien' => ALIEN_COLORS,
153
+ ## more series
154
+ 'vampire' => VAMPIRE_COLORS,
155
+ 'mummy' => MUMMY_COLORS,
156
+ 'orc' => ORC_COLORS,
157
+ 'skeleton' => SKELETON_COLORS,
158
+ 'demon' => DEMON_COLORS,
159
+ 'robot' => ROBOT_COLORS,
160
+ }
161
+
162
+
@@ -0,0 +1,39 @@
1
+ module Cryptopunks
2
+ class Image ## nest Composite inside Image - why? why not?
3
+ class Composite < Pixelart::ImageComposite
4
+
5
+ PUNK_HASH = 'ac39af4793119ee46bbff351d8cb6b5f23da60222126add4268e261199a2921b'
6
+
7
+ def self.sha256( data )
8
+ ## todo/check: or just use Digest::SHA256.hexdigest - why? why not?
9
+ Digest::SHA256.digest( data ).unpack( 'H*' )[0]
10
+ end
11
+
12
+
13
+ PUNK_HEIGHT = 24
14
+ PUNK_WIDTH = 24
15
+
16
+ def self.read( path='./punks.png', width: PUNK_WIDTH, height: PUNK_HEIGHT )
17
+ data = File.open( path, 'rb' ) { |f| f.read }
18
+
19
+ hexdigest = sha256( data ) ## check sha256 checksum
20
+ if hexdigest == PUNK_HASH
21
+ puts " >#{hexdigest}< SHA256 hash matching"
22
+ puts " ✓ True Official Genuine CryptoPunks™ verified"
23
+ else
24
+ puts " ✓ True Official Genuine Yes, You Can! Punks Not Dead™ verified"
25
+ end
26
+
27
+ img = ChunkyPNG::Image.from_blob( data )
28
+ new( img, width: width, height: height )
29
+ end
30
+
31
+
32
+ def initialize( *args, width: PUNK_WIDTH,
33
+ height: PUNK_HEIGHT )
34
+ super
35
+ end
36
+
37
+ end ## class Composite
38
+ end ## class Image
39
+ end ## module Cryptopunks
@@ -0,0 +1,204 @@
1
+
2
+ module Cryptopunks
3
+
4
+ class Metadata
5
+ ### todo/fix:
6
+ ## move into Punks::Metadata or such
7
+ class Sprite
8
+ attr_reader :id, :name, :type, :gender
9
+
10
+
11
+ def initialize( id:,
12
+ name:,
13
+ type:,
14
+ gender: )
15
+ @id = id # zero-based index eg. 0,1,2,3, etc.
16
+ @name = name
17
+ @type = type
18
+ @gender = gender
19
+ end
20
+
21
+ ## todo/check - find better names for type attribute/archetypes?
22
+ ## use (alternate name/alias) base or face for archetypes? any others?
23
+ def attribute?() @type.downcase.start_with?( 'attribute' ); end
24
+ def archetype?() @type.downcase.start_with?( 'archetype' ); end
25
+ end # class Metadata::Sprite
26
+ end # class Metadata
27
+
28
+
29
+
30
+
31
+ class Generator
32
+
33
+ ######
34
+ # static helpers - (turn into "true" static self.class methods - why? why not?)
35
+ #
36
+ def normalize_key( str )
37
+ str.downcase.gsub(/[ ()°_-]/, '').strip
38
+ end
39
+
40
+ def normalize_gender( str )
41
+ ## e.g. Female => f
42
+ ## F => f
43
+ ## always return f or m
44
+ str.downcase[0]
45
+ end
46
+
47
+
48
+ def build_attributes_by_name( recs )
49
+ h = {}
50
+ recs.each_with_index do |rec|
51
+ key = normalize_key( rec.name )
52
+ key << "_(#{rec.gender})" if rec.attribute?
53
+
54
+ if h[ key ]
55
+ puts "!!! ERROR - attribute name is not unique:"
56
+ pp rec
57
+ puts "duplicate:"
58
+ pp h[key]
59
+ exit 1
60
+ end
61
+ h[ key ] = rec
62
+ end
63
+ ## pp h
64
+ h
65
+ end
66
+
67
+
68
+ def build_recs( recs ) ## build and normalize (meta data) records
69
+
70
+ ## sort by id
71
+ recs = recs.sort do |l,r|
72
+ l['id'].to_i( 10 ) <=> r['id'].to_i( 10 ) # use base10 (decimal)
73
+ end
74
+
75
+ ## assert all recs are in order by id (0 to size)
76
+ recs.each_with_index do |rec, exp_id|
77
+ id = rec['id'].to_i(10)
78
+ if id != exp_id
79
+ puts "!! ERROR - meta data record ids out-of-order - expected id #{exp_id}; got #{id}"
80
+ exit 1
81
+ end
82
+ end
83
+
84
+ ## convert to "wrapped / immutable" kind-of struct
85
+ recs = recs.map do |rec|
86
+ id = rec['id'].to_i( 10 )
87
+ name = rec['name']
88
+ gender = normalize_gender( rec['gender'] )
89
+ type = rec['type']
90
+
91
+ Metadata::Sprite.new(
92
+ id: id,
93
+ name: name,
94
+ type: type,
95
+ gender: gender)
96
+ end
97
+ recs
98
+ end # method build_recs
99
+
100
+
101
+
102
+
103
+ def initialize( image_path="./spritesheet.png",
104
+ meta_path="./spritesheet.csv" )
105
+ @sheet = Pixelart::ImageComposite.read( image_path )
106
+ recs = CsvHash.read( meta_path )
107
+
108
+ @recs = build_recs( recs )
109
+
110
+ ## lookup by "case/space-insensitive" name / key
111
+ @attributes_by_name = build_attributes_by_name( @recs )
112
+ end
113
+
114
+
115
+ def spritesheet() @sheet; end
116
+ alias_method :sheet, :spritesheet
117
+
118
+
119
+
120
+
121
+
122
+ def find_meta( q, gender: nil ) ## gender (m/f) required for attributes!!!
123
+
124
+ key = normalize_key( q ) ## normalize q(uery) string/symbol
125
+ key << "_(#{normalize_gender( gender )})" if gender
126
+
127
+ rec = @attributes_by_name[ key ]
128
+ if rec
129
+ puts " lookup >#{key}< => #{rec.id}: #{rec.name} / #{rec.type} (#{rec.gender})"
130
+ # pp rec
131
+ else
132
+ puts "!! WARN - no lookup found for key >#{key}<"
133
+ end
134
+ rec
135
+ end
136
+
137
+
138
+ def find( q, gender: nil ) ## gender (m/f) required for attributes!!!
139
+ rec = find_meta( q, gender: gender )
140
+
141
+ ## return image if record found
142
+ rec ? @sheet[ rec.id ] : nil
143
+ end
144
+
145
+
146
+
147
+
148
+ def to_recs( *values )
149
+ archetype_name = values[0]
150
+
151
+ ### todo/fix: check for nil/not found!!!!
152
+ archetype = find_meta( archetype_name )
153
+ if archetype.nil?
154
+ puts "!! ERROR - archetype >#{archetype}< not found; sorry"
155
+ exit 1
156
+ end
157
+
158
+ recs = [archetype]
159
+
160
+ attribute_names = values[1..-1]
161
+ ## note: attribute lookup requires gender from archetype!!!!
162
+ attribute_gender = archetype.gender
163
+
164
+ attribute_names.each do |attribute_name|
165
+ attribute = find_meta( attribute_name, gender: attribute_gender )
166
+ if attribute.nil?
167
+ puts "!! ERROR - attribute >#{attribute_name}< for (#{attribute_gender}) not found; sorry"
168
+ exit 1
169
+ end
170
+ recs << attribute
171
+ end
172
+
173
+ recs
174
+ end
175
+
176
+
177
+
178
+
179
+ def generate_image( *values, background: nil )
180
+
181
+ ids = if values[0].is_a?( Integer ) ## assume integer number (indexes)
182
+ values
183
+ else ## assume strings (names)
184
+ to_recs( *values ).map { |rec| rec.id }
185
+ end
186
+
187
+
188
+ punk = Pixelart::Image.new( 24, 24 )
189
+
190
+ if background ## for now assume background is (simply) color
191
+ punk.compose!( Pixelart::Image.new( 24, 24, background ) )
192
+ end
193
+
194
+ ids.each do |id|
195
+ punk.compose!( @sheet[ id ] )
196
+ end
197
+
198
+ punk
199
+ end
200
+ alias_method :generate, :generate_image
201
+
202
+ end # class Generator
203
+
204
+ end # module Cryptopunks
@@ -1,84 +1,121 @@
1
1
  module Cryptopunks
2
- class Image
3
- def self.read( path='./punks.png' )
4
- data = File.open( path, 'rb' ) { |f| f.read }
5
- new( data )
6
- end
7
-
8
-
9
- attr_accessor :zoom
10
2
 
11
- PUNK_ROWS = 100
12
- PUNK_COLS = 100
13
- PUNK_COUNT = PUNK_ROWS * PUNK_COLS ## 10_000 = 100x100 (24000x24000 pixel)
14
3
 
15
- PUNK_HEIGHT = 24
16
- PUNK_WIDTH = 24
4
+ class Design ## todo/fix - move to its own file!!!
17
5
 
18
- PUNK_HASH = 'ac39af4793119ee46bbff351d8cb6b5f23da60222126add4268e261199a2921b'
6
+ end # class Design
19
7
 
20
8
 
21
- def initialize( data )
22
- @punks = ChunkyPNG::Image.from_blob( data )
23
- puts " #{@punks.height}x#{@punks.width} (height x width)"
24
-
25
- ## check sha256 checksum
26
- @hexdigest = sha256( data )
27
- if original?
28
- puts " >#{@hexdigest}< SHA256 hash matching"
29
- puts " ✓ True Official Genuine CryptoPunks™ verified"
30
- else
31
- puts " !! ERROR: >#{hexdigest}< SHA256 hash NOT matching"
32
- puts " >#{PUNK_HASH}< expected for True Official Genuine CryptoPunks™."
33
- puts ""
34
- puts " Sorry, please download the original."
35
- exit 1
36
- end
37
9
 
38
- @zoom = 1
10
+ ##############
11
+ ## todo/check:
12
+ ## find a better way to (auto?) include more designs?
13
+ class DesignSeries ## find a better name for class (just use Series?) - why? why not?
14
+ def self.build( dir )
15
+ data = {}
16
+ paths = Dir.glob( "#{dir}/**.txt" )
17
+ paths.each do |path|
18
+ basename = File.basename( path, File.extname( path ) )
19
+ text = File.open( path, 'r:utf-8' ) { |f| f.read }
20
+ ## todo/check: auto-parse "ahead of time" here
21
+ ## or keep "raw" text - why? why not?
22
+ data[ basename ] = text
39
23
  end
24
+ data
25
+ end
26
+
27
+ def initialize( dir )
28
+ @dir = dir # e.g. "#{Cryptopunks.root}/config/more"
29
+ end
30
+
31
+ def data
32
+ ## note: lazy load / build on first demand only
33
+ @data ||= self.class.build( @dir )
34
+ end
35
+
36
+ def [](key) data[ key ]; end
37
+ def size() data.size; end
38
+ def keys() data.keys; end
39
+ def to_h() data; end ## todo/check: use to_hash() - why? why not?
40
+ end # class DesignSeries
41
+
42
+
43
+
44
+ class Image
45
+
46
+ def self.read( path ) ## convenience helper
47
+ img = ChunkyPNG::Image.from_file( path )
48
+ new( img )
49
+ end
50
+
51
+
52
+ def initialize( initial=nil, design: nil,
53
+ colors: nil )
54
+ if initial
55
+ ## pass image through as-is
56
+ img = initial
57
+ else
58
+
59
+ ## todo/fix:
60
+ ## move design code into design class!!!
61
+ ## for now assume design is a string
62
+ ## split into parts
63
+ ## original/alien-male or original@alien-male
64
+ ## more/alien-female or more@alien-female
65
+ ## original/human-male+darker or original@human-male!darker ????
66
+ ## human-male!darker ?????
67
+ ## keep @ as separator too - why? why not?
68
+ parts = design.split( %r{[@/]} )
69
+ parts.unshift( '*' ) if parts.size == 1 ## assume "all-in-one" series (use * as name/id/placeholder)
70
+
71
+ series_key = parts[0]
72
+ design_composite = parts[1]
73
+
74
+ ## todo/check - find a way for unambigious (color) variant key
75
+ ## use unique char e.g. +*!# or such
76
+ more_parts = design_composite.split( %r{[!+]} )
77
+ design_key = more_parts[0]
78
+ variant_key = more_parts[1] ## color variant for now (for humans) e.g. lighter/light/dark/darker
79
+
80
+ series = if ['*','**','_','__'].include?( series_key )
81
+ DESIGNS ## use all-series-in-one collection
82
+ else
83
+ case series_key
84
+ when 'original' then DESIGNS_ORIGINAL
85
+ when 'more' then DESIGNS_MORE
86
+ else raise ArgumentError, "unknown design series >#{series_key}<; sorry"
87
+ end
88
+ end
89
+
90
+ design = series[ design_key ]
91
+ raise ArgumentError, "unknow design >#{design_key}< in series >#{series_key}<; sorry" if design.nil?
92
+
93
+ if colors.nil? ## try to auto-fill in colors
94
+ ## note: (auto-)remove _male,_female qualifier if exist
95
+ colors_key = design_key.sub( '-male', '' ).sub( '-female', '' )
96
+ colors = COLORS[ colors_key ]
97
+
98
+ ## allow / support color scheme variants (e.g. lighter/light/dark/darker) etc.
99
+ if colors.is_a?(Hash)
100
+ if variant_key
101
+ colors = colors[ variant_key ]
102
+ raise ArgumentError, "no colors defined for variant >#{variant_key}< for design >#{design_key}< in series >#{series_key}<; sorry" if colors.nil?
103
+ else ## note: use (fallback to) first color scheme if no variant key present
104
+ colors = colors[ colors.keys[0] ]
105
+ end
106
+ end
107
+
108
+ raise ArgumentError, "no (default) colors defined for design >#{design_key}< in series >#{series_key}<; sorry" if colors.nil?
109
+ end
40
110
 
41
-
42
- def hexdigest() @hexdigest end
43
-
44
- def verify?() @hexdigest == PUNK_HASH; end
45
- alias_method :genuine?, :verify?
46
- alias_method :original?, :verify?
47
-
48
-
49
-
50
- def size() PUNK_COUNT; end
51
-
52
- def []( index )
53
- @zoom == 1 ? crop( index ) : scale( index, @zoom )
54
- end
55
-
56
-
57
- def crop( index )
58
- y, x = index.divmod( PUNK_ROWS )
59
- @punks.crop( x*PUNK_WIDTH, y*PUNK_HEIGHT, PUNK_WIDTH, PUNK_HEIGHT )
111
+ ## note: unwrap inner image before passing on to super c'tor
112
+ img = Pixelart::Image.parse( design, colors: colors ).image
60
113
  end
61
114
 
115
+ super( img.width, img.height, img )
116
+ end
62
117
 
63
- def scale( index, zoom )
64
- punk = ChunkyPNG::Image.new( PUNK_WIDTH*zoom, PUNK_HEIGHT*zoom,
65
- ChunkyPNG::Color::WHITE )
66
118
 
67
- ## (x,y) offset in big all-in-one punks image
68
- y, x = index.divmod( PUNK_ROWS )
69
119
 
70
- ## copy all 24x24 pixels
71
- PUNK_WIDTH.times do |i|
72
- PUNK_HEIGHT.times do |j|
73
- pixel = @punks[i+x*PUNK_WIDTH, j+y*PUNK_HEIGHT]
74
- zoom.times do |n|
75
- zoom.times do |m|
76
- punk[n+zoom*i,m+zoom*j] = pixel
77
- end
78
- end
79
- end
80
- end
81
- punk
82
- end
83
- end ## class Image
84
- end ## module Cryptopunks
120
+ end # class Image
121
+ end # module Cryptopunks
@@ -137,12 +137,25 @@ end ## (nested) class Accessory
137
137
  @birthday = Date.new( 2017, 6, 23) ## all 10,000 minted on June 23, 2017
138
138
  end
139
139
 
140
- ## convenience helpers for types (5)
141
- def alien?() @type.name=='Alien'; end
142
- def ape?() @type.name=='Ape'; end
143
- def zombie?() @type.name=='Zombie'; end
144
- def female?() @type.name=='Female'; end
145
- def male?() @type.name=='Male'; end
140
+ def is_type?( name ) @type.name == name; end
141
+ alias_method :is?, :is_type?
142
+
143
+ ## convenience helpers for "classic" (5) types
144
+ def alien?() is_type?( 'Alien'); end
145
+ def ape?() is_type?( 'Ape' ); end
146
+ def zombie?() is_type?( 'Zombie' ); end
147
+ def female?() is_type?( 'Female' ); end
148
+ def male?() is_type?( 'Male' ); end
149
+
150
+ ## convenience helpers to lookup attributes
151
+ def has_attribute?( name )
152
+ accessories.each do |acc|
153
+ return true if acc.name == name
154
+ end
155
+ false
156
+ end
157
+ alias_method :has?, :has_attribute?
158
+ alias_method :include?, :has_attribute?
146
159
  end # class Metadata
147
160
 
148
161
  end # module Cryptopunks
@@ -3,8 +3,8 @@
3
3
  module Cryptopunks
4
4
 
5
5
  MAJOR = 1
6
- MINOR = 1
7
- PATCH = 0
6
+ MINOR = 2
7
+ PATCH = 2
8
8
  VERSION = [MAJOR,MINOR,PATCH].join('.')
9
9
 
10
10
  def self.version