cryptopunks 1.2.0 → 2.0.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.
@@ -0,0 +1,207 @@
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
+ def records() @recs; end
120
+ alias_method :meta, :records
121
+
122
+
123
+
124
+
125
+ def find_meta( q, gender: nil ) ## gender (m/f) required for attributes!!!
126
+
127
+ key = normalize_key( q ) ## normalize q(uery) string/symbol
128
+ key << "_(#{normalize_gender( gender )})" if gender
129
+
130
+ rec = @attributes_by_name[ key ]
131
+ if rec
132
+ puts " lookup >#{key}< => #{rec.id}: #{rec.name} / #{rec.type} (#{rec.gender})"
133
+ # pp rec
134
+ else
135
+ puts "!! WARN - no lookup found for key >#{key}<"
136
+ end
137
+ rec
138
+ end
139
+
140
+
141
+ def find( q, gender: nil ) ## gender (m/f) required for attributes!!!
142
+ rec = find_meta( q, gender: gender )
143
+
144
+ ## return image if record found
145
+ rec ? @sheet[ rec.id ] : nil
146
+ end
147
+
148
+
149
+
150
+
151
+ def to_recs( *values )
152
+ archetype_name = values[0]
153
+
154
+ ### todo/fix: check for nil/not found!!!!
155
+ archetype = find_meta( archetype_name )
156
+ if archetype.nil?
157
+ puts "!! ERROR - archetype >#{archetype}< not found; sorry"
158
+ exit 1
159
+ end
160
+
161
+ recs = [archetype]
162
+
163
+ attribute_names = values[1..-1]
164
+ ## note: attribute lookup requires gender from archetype!!!!
165
+ attribute_gender = archetype.gender
166
+
167
+ attribute_names.each do |attribute_name|
168
+ attribute = find_meta( attribute_name, gender: attribute_gender )
169
+ if attribute.nil?
170
+ puts "!! ERROR - attribute >#{attribute_name}< for (#{attribute_gender}) not found; sorry"
171
+ exit 1
172
+ end
173
+ recs << attribute
174
+ end
175
+
176
+ recs
177
+ end
178
+
179
+
180
+
181
+
182
+ def generate_image( *values, background: nil )
183
+
184
+ ids = if values[0].is_a?( Integer ) ## assume integer number (indexes)
185
+ values
186
+ else ## assume strings (names)
187
+ to_recs( *values ).map { |rec| rec.id }
188
+ end
189
+
190
+
191
+ punk = Pixelart::Image.new( 24, 24 )
192
+
193
+ if background ## for now assume background is (simply) color
194
+ punk.compose!( Pixelart::Image.new( 24, 24, background ) )
195
+ end
196
+
197
+ ids.each do |id|
198
+ punk.compose!( @sheet[ id ] )
199
+ end
200
+
201
+ punk
202
+ end
203
+ alias_method :generate, :generate_image
204
+
205
+ end # class Generator
206
+
207
+ end # module Cryptopunks
@@ -0,0 +1,33 @@
1
+ module Cryptopunks
2
+
3
+
4
+
5
+ class Image
6
+
7
+ def self.read( path ) ## convenience helper
8
+ img = ChunkyPNG::Image.from_file( path )
9
+ new( img )
10
+ end
11
+
12
+
13
+
14
+ ### keep design & colors keyword args in c'tor here
15
+ ## or use parse() like in pixelart - why? why not?
16
+
17
+ def initialize( initial=nil, design: nil,
18
+ colors: nil )
19
+ if initial
20
+ ## pass image through as-is
21
+ img = initial
22
+ else
23
+ ## note: unwrap inner image before passing on to super c'tor
24
+ img = Pixelart::Image.parse( design, colors: colors ).image
25
+ end
26
+
27
+ super( img.width, img.height, img )
28
+ end
29
+
30
+
31
+
32
+ end # class Image
33
+ 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
@@ -0,0 +1,264 @@
1
+ module Cryptopunks
2
+
3
+
4
+
5
+ class Tool
6
+ def run( args )
7
+ Toolii.run( args )
8
+ end
9
+ end
10
+
11
+
12
+
13
+ class Opts
14
+ def merge_gli_options!( options = {} )
15
+ # puts " update options:"
16
+ # puts options.inspect
17
+
18
+ @file = options[:file] if options[:file]
19
+ @outdir = options[:dir] if options[:dir]
20
+
21
+ @zoom = options[:zoom] if options[:zoom]
22
+ @offset = options[:offset] if options[:offset]
23
+
24
+ @verbose = true if options[:verbose] == true
25
+ end
26
+
27
+
28
+ def verbose=(boolean) # add: alias for debug ??
29
+ @verbose = boolean
30
+ end
31
+
32
+ def verbose?
33
+ return false if @verbose.nil? # default verbose/debug flag is false
34
+ @verbose == true
35
+ end
36
+
37
+ def file() @file || './punks.png'; end
38
+ def file?() @file; end ## note: let's you check if file is set (or "untouched")
39
+
40
+ def zoom() @zoom || 1; end
41
+ def zoom?() @zoom; end
42
+
43
+ def offset() @offset || 0; end
44
+ def offset?() @offset; end
45
+
46
+ def outdir() @outdir || '.'; end
47
+ def outdir?() @outdir; end
48
+ end # class Opts
49
+
50
+
51
+
52
+ ## note: use gli "dsl" inside a class / namespace
53
+ class Toolii
54
+ extend GLI::App
55
+
56
+ opts = Opts.new
57
+
58
+
59
+ program_desc 'punk (or cryptopunk) command line tool'
60
+
61
+ version Cryptopunks::VERSION
62
+
63
+
64
+ desc "Zoom factor x2, x4, x8, etc."
65
+ arg_name 'ZOOM'
66
+ default_value opts.zoom
67
+ flag [:z, :zoom], type: Integer
68
+
69
+ desc "Start counting at offset"
70
+ arg_name 'NUM'
71
+ default_value opts.offset
72
+ flag [:offset], type: Integer
73
+
74
+ desc "Output directory"
75
+ arg_name 'DIR'
76
+ default_value opts.outdir
77
+ flag [:d, :dir,
78
+ :o, :out, :outdir], type: String
79
+
80
+ ### todo/check: move option to -t/--tile command only - why? why not?
81
+ desc "True Official Genuine CryptoPunks™ all-in-one composite image"
82
+ arg_name 'FILE'
83
+ default_value opts.file
84
+ flag [:f, :file], type: String
85
+
86
+
87
+
88
+ ### global option (required)
89
+ ## todo: add check that path is valid?? possible?
90
+ desc '(Debug) Show debug messages'
91
+ switch [:verbose], negatable: false ## todo: use -w for short form? check ruby interpreter if in use too?
92
+
93
+
94
+
95
+ desc "Get punk characters via image tiles from all-in-one punk series composite (#{opts.file}) - for IDs use 0 to 9999"
96
+ command [:t, :tile] do |c|
97
+ c.action do |g,o,args|
98
+
99
+ # puts "opts:"
100
+ # puts opts.inspect
101
+
102
+ puts "==> reading >#{opts.file}<..."
103
+ punks = ImageComposite.read( opts.file )
104
+
105
+
106
+ puts " setting zoom to #{opts.zoom}x" if opts.zoom != 1
107
+
108
+ ## make sure outdir exits (default is current working dir e.g. .)
109
+ FileUtils.mkdir_p( opts.outdir ) unless Dir.exist?( opts.outdir )
110
+
111
+ args.each_with_index do |arg,index|
112
+ punk_index = arg.to_i( 10 ) ## assume base 10 decimal
113
+
114
+ punk = punks[ punk_index ]
115
+
116
+ punk_name = "punk-" + "%04d" % (punk_index + opts.offset)
117
+
118
+ ## if zoom - add x2,x4 or such
119
+ if opts.zoom != 1
120
+ punk = punk.zoom( opts.zoom )
121
+ punk_name << "@#{opts.zoom}x"
122
+ end
123
+
124
+ path = "#{opts.outdir}/#{punk_name}.png"
125
+ puts "==> (#{index+1}/#{args.size}) saving punk ##{punk_index+opts.offset} to >#{path}<..."
126
+
127
+ punk.save( path )
128
+ end
129
+ puts 'Done.'
130
+ end # action
131
+ end # command tile
132
+
133
+
134
+
135
+ desc 'Generate punk characters from text attributes (from scratch / zero) via builtin punk spritesheet'
136
+ command [:g, :gen, :generate] do |c|
137
+ c.action do |g,o,args|
138
+
139
+ puts "==> generating >#{args.join( ' + ' )}<..."
140
+ punk = Image.generate( *args )
141
+
142
+ puts " setting zoom to #{opts.zoom}x" if opts.zoom != 1
143
+
144
+ ## make sure outdir exits (default is current working dir e.g. .)
145
+ FileUtils.mkdir_p( opts.outdir ) unless Dir.exist?( opts.outdir )
146
+
147
+ punk_index = 0 ## assume base 10 decimal
148
+ punk_name = "punk-" + "%04d" % (punk_index + opts.offset)
149
+
150
+ ## if zoom - add x2,x4 or such
151
+ if opts.zoom != 1
152
+ punk = punk.zoom( opts.zoom )
153
+ punk_name << "@#{opts.zoom}x"
154
+ end
155
+
156
+ path = "#{opts.outdir}/#{punk_name}.png"
157
+ puts "==> saving punk ##{punk_index+opts.offset} to >#{path}<..."
158
+
159
+ punk.save( path )
160
+ puts 'Done.'
161
+ end # action
162
+ end # command generate
163
+
164
+
165
+ desc 'Query (builtin off-chain) punk contract for punk text attributes by IDs - use 0 to 9999'
166
+ command [:q, :query] do |c|
167
+ c.action do |g,o,args|
168
+
169
+ # puts "opts:"
170
+ # puts opts.inspect
171
+
172
+ args.each_with_index do |arg,index|
173
+ punk_index = arg.to_i( 10 ) ## assume base 10 decimal
174
+
175
+ puts "==> (#{index+1}/#{args.size}) punk ##{punk_index}..."
176
+
177
+ attribute_names = CryptopunksData.punk_attributes( punk_index )
178
+ ## downcase name and change spaces to underscore
179
+ attribute_names = attribute_names.map do |name|
180
+ name.downcase.gsub( ' ', '_' )
181
+ end
182
+
183
+ print " "
184
+ print attribute_names.join( ' ' )
185
+ print "\n"
186
+ end
187
+ puts 'Done.'
188
+ end
189
+ end
190
+
191
+
192
+
193
+ desc 'List all punk archetype and attribute names from builtin punk spritesheet'
194
+ command [:l, :ls, :list] do |c|
195
+ c.action do |g,o,args|
196
+
197
+ generator = Cryptopunks.generator
198
+
199
+ puts "==> Archetypes"
200
+ generator.meta.each do |rec|
201
+ next unless rec.archetype?
202
+
203
+ print " "
204
+ print "%-30s" % "#{rec.name} / (#{rec.gender})"
205
+ print " - #{rec.type}"
206
+ print "\n"
207
+ end
208
+
209
+ puts ""
210
+ puts "==> Attributes"
211
+ generator.meta.each do |rec|
212
+ next unless rec.attribute?
213
+
214
+ print " "
215
+ print "%-30s" % "#{rec.name} / (#{rec.gender})"
216
+ print " - #{rec.type}"
217
+ print "\n"
218
+ end
219
+
220
+ puts ""
221
+ puts " See github.com/cryptopunksnotdead/punks.spritesheet for more."
222
+ puts ""
223
+
224
+ puts 'Done.'
225
+ end # action
226
+ end # command list
227
+
228
+
229
+
230
+ pre do |g,c,o,args|
231
+ opts.merge_gli_options!( g )
232
+ opts.merge_gli_options!( o )
233
+
234
+ if opts.verbose?
235
+ ## LogUtils::Logger.root.level = :debug
236
+ end
237
+
238
+ ## logger.debug "Executing #{c.name}"
239
+ true
240
+ end
241
+
242
+ post do |global,c,o,args|
243
+ ## logger.debug "Executed #{c.name}"
244
+ true
245
+ end
246
+
247
+
248
+ on_error do |e|
249
+ puts
250
+ puts "*** error: #{e.message}"
251
+
252
+ if opts.verbose?
253
+ puts e.backtrace
254
+ end
255
+
256
+ false # skip default error handling
257
+ end
258
+
259
+
260
+ ### exit run(ARGV) ## note: use Toolii.run( ARGV ) outside of class
261
+ end # class Toolii
262
+ end # module Cryptopunks
263
+
264
+
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Cryptopunks
4
4
 
5
- MAJOR = 1
6
- MINOR = 2
5
+ MAJOR = 2
6
+ MINOR = 0
7
7
  PATCH = 0
8
8
  VERSION = [MAJOR,MINOR,PATCH].join('.')
9
9