cryptopunks 1.1.0 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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