cryptopunks 1.2.1 → 2.0.1

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,223 @@
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, :more_names
9
+
10
+
11
+ def initialize( id:,
12
+ name:,
13
+ type:,
14
+ gender:,
15
+ more_names: [] )
16
+ @id = id # zero-based index eg. 0,1,2,3, etc.
17
+ @name = name
18
+ @type = type
19
+ @gender = gender
20
+ @more_names = more_names
21
+ end
22
+
23
+ ## todo/check - find better names for type attribute/archetypes?
24
+ ## use (alternate name/alias) base or face for archetypes? any others?
25
+ def attribute?() @type.downcase.start_with?( 'attribute' ); end
26
+ def archetype?() @type.downcase.start_with?( 'archetype' ); end
27
+ end # class Metadata::Sprite
28
+ end # class Metadata
29
+
30
+
31
+
32
+
33
+ class Generator
34
+
35
+ ######
36
+ # static helpers - (turn into "true" static self.class methods - why? why not?)
37
+ #
38
+ def normalize_key( str )
39
+ str.downcase.gsub(/[ ()°_-]/, '').strip
40
+ end
41
+
42
+ def normalize_gender( str )
43
+ ## e.g. Female => f
44
+ ## F => f
45
+ ## always return f or m
46
+ str.downcase[0]
47
+ end
48
+
49
+ def normalize_name( str )
50
+ ## normalize spaces in more names
51
+ str.strip.gsub( /[ ]{2,}/, ' ' )
52
+ end
53
+
54
+
55
+
56
+ def build_attributes_by_name( recs )
57
+ h = {}
58
+ recs.each_with_index do |rec|
59
+ names = [rec.name] + rec.more_names
60
+ names.each do |name|
61
+
62
+ key = normalize_key( name )
63
+ key << "_(#{rec.gender})" if rec.attribute?
64
+
65
+ if h[ key ]
66
+ puts "!!! ERROR - attribute name is not unique:"
67
+ pp rec
68
+ puts "duplicate:"
69
+ pp h[key]
70
+ exit 1
71
+ end
72
+ h[ key ] = rec
73
+ end
74
+ end
75
+ ## pp h
76
+ h
77
+ end
78
+
79
+
80
+ def build_recs( recs ) ## build and normalize (meta data) records
81
+
82
+ ## sort by id
83
+ recs = recs.sort do |l,r|
84
+ l['id'].to_i( 10 ) <=> r['id'].to_i( 10 ) # use base10 (decimal)
85
+ end
86
+
87
+ ## assert all recs are in order by id (0 to size)
88
+ recs.each_with_index do |rec, exp_id|
89
+ id = rec['id'].to_i(10)
90
+ if id != exp_id
91
+ puts "!! ERROR - meta data record ids out-of-order - expected id #{exp_id}; got #{id}"
92
+ exit 1
93
+ end
94
+ end
95
+
96
+ ## convert to "wrapped / immutable" kind-of struct
97
+ recs = recs.map do |rec|
98
+ id = rec['id'].to_i( 10 )
99
+ name = normalize_name( rec['name'] )
100
+ gender = normalize_gender( rec['gender'] )
101
+ type = rec['type']
102
+
103
+ more_names = (rec['more_names'] || '').split( '|' )
104
+ more_names = more_names.map {|str| normalize_name( str ) }
105
+
106
+ Metadata::Sprite.new(
107
+ id: id,
108
+ name: name,
109
+ type: type,
110
+ gender: gender,
111
+ more_names: more_names )
112
+ end
113
+ recs
114
+ end # method build_recs
115
+
116
+
117
+
118
+
119
+ def initialize( image_path="./spritesheet.png",
120
+ meta_path="./spritesheet.csv" )
121
+ @sheet = Pixelart::ImageComposite.read( image_path )
122
+ recs = CsvHash.read( meta_path )
123
+
124
+ @recs = build_recs( recs )
125
+
126
+ ## lookup by "case/space-insensitive" name / key
127
+ @attributes_by_name = build_attributes_by_name( @recs )
128
+ end
129
+
130
+
131
+ def spritesheet() @sheet; end
132
+ alias_method :sheet, :spritesheet
133
+
134
+
135
+ def records() @recs; end
136
+ alias_method :meta, :records
137
+
138
+
139
+
140
+
141
+ def find_meta( q, gender: nil ) ## gender (m/f) required for attributes!!!
142
+
143
+ key = normalize_key( q ) ## normalize q(uery) string/symbol
144
+ key << "_(#{normalize_gender( gender )})" if gender
145
+
146
+ rec = @attributes_by_name[ key ]
147
+ if rec
148
+ puts " lookup >#{key}< => #{rec.id}: #{rec.name} / #{rec.type} (#{rec.gender})"
149
+ # pp rec
150
+ else
151
+ puts "!! WARN - no lookup found for key >#{key}<"
152
+ end
153
+ rec
154
+ end
155
+
156
+
157
+ def find( q, gender: nil ) ## gender (m/f) required for attributes!!!
158
+ rec = find_meta( q, gender: gender )
159
+
160
+ ## return image if record found
161
+ rec ? @sheet[ rec.id ] : nil
162
+ end
163
+
164
+
165
+
166
+
167
+ def to_recs( *values )
168
+ archetype_name = values[0]
169
+
170
+ ### todo/fix: check for nil/not found!!!!
171
+ archetype = find_meta( archetype_name )
172
+ if archetype.nil?
173
+ puts "!! ERROR - archetype >#{archetype}< not found; sorry"
174
+ exit 1
175
+ end
176
+
177
+ recs = [archetype]
178
+
179
+ attribute_names = values[1..-1]
180
+ ## note: attribute lookup requires gender from archetype!!!!
181
+ attribute_gender = archetype.gender
182
+
183
+ attribute_names.each do |attribute_name|
184
+ attribute = find_meta( attribute_name, gender: attribute_gender )
185
+ if attribute.nil?
186
+ puts "!! ERROR - attribute >#{attribute_name}< for (#{attribute_gender}) not found; sorry"
187
+ exit 1
188
+ end
189
+ recs << attribute
190
+ end
191
+
192
+ recs
193
+ end
194
+
195
+
196
+
197
+
198
+ def generate_image( *values, background: nil )
199
+
200
+ ids = if values[0].is_a?( Integer ) ## assume integer number (indexes)
201
+ values
202
+ else ## assume strings (names)
203
+ to_recs( *values ).map { |rec| rec.id }
204
+ end
205
+
206
+
207
+ punk = Pixelart::Image.new( 24, 24 )
208
+
209
+ if background ## for now assume background is (simply) color
210
+ punk.compose!( Pixelart::Image.new( 24, 24, background ) )
211
+ end
212
+
213
+ ids.each do |id|
214
+ punk.compose!( @sheet[ id ] )
215
+ end
216
+
217
+ punk
218
+ end
219
+ alias_method :generate, :generate_image
220
+
221
+ end # class Generator
222
+
223
+ end # module Cryptopunks
@@ -1,45 +1,6 @@
1
1
  module Cryptopunks
2
2
 
3
3
 
4
- class Design ## todo/fix - move to its own file!!!
5
-
6
- end # class Design
7
-
8
-
9
-
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
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
4
 
44
5
  class Image
45
6
 
@@ -49,65 +10,16 @@ def self.read( path ) ## convenience helper
49
10
  end
50
11
 
51
12
 
13
+
14
+ ### keep design & colors keyword args in c'tor here
15
+ ## or use parse() like in pixelart - why? why not?
16
+
52
17
  def initialize( initial=nil, design: nil,
53
18
  colors: nil )
54
19
  if initial
55
20
  ## pass image through as-is
56
- img = inital
21
+ img = initial
57
22
  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
110
-
111
23
  ## note: unwrap inner image before passing on to super c'tor
112
24
  img = Pixelart::Image.parse( design, colors: colors ).image
113
25
  end
@@ -0,0 +1,275 @@
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
+
250
+ if opts.verbose?
251
+ puts e.backtrace
252
+ end
253
+
254
+ if e.is_a?( SystemExit )
255
+ puts
256
+ puts "*** error: system exit with status code ( #{e.status} )"
257
+ exit( e.status ) ## try exit again to make sure error code gets passed along!!!
258
+ else
259
+ puts
260
+ puts "*** error: #{e.message}"
261
+ end
262
+
263
+ ## note: was false # skip default error handling
264
+
265
+ ## note: try true - false WILL SWALLOW exit codes and such
266
+ ## - looks like it's still returning 0 (e.g. on unknown option or such)!!!!
267
+ true
268
+ end
269
+
270
+
271
+ ### exit run(ARGV) ## note: use Toolii.run( ARGV ) outside of class
272
+ end # class Toolii
273
+ end # module Cryptopunks
274
+
275
+
@@ -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 = 1
8
8
  VERSION = [MAJOR,MINOR,PATCH].join('.')
9
9