cryptopunks 1.2.1 → 2.0.1

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