Hacking EVE, Part 3 – Let’s Build Something!

ore-mackinaw-2

Welcome back once again, EVE industrialists and folks who like to script in Ruby! So far in our series we’ve read static data from a database export and used web APIs to read dynamic pricing data. Today let’s put those things together to compute cost and profit for a couple of common items. We’ll look at traversing trees with visitors and sorting and iterating over nodes with blocks. The full source code is available on github.

Our Script

For today’s script, what we’d like to do is specify a couple items that we’d like to build, then traverse over our build tree computing the materials and pricing data associated with each node. Our report script will look like:

# Create our pricing data.
markets = CompositePricingModel.new(
	['Jita', 'Amarr'].map do |system|
		PersistentPricingModel.new(
			LowSellOrdersPricingModel.new(
				MapSolarSystems.find_by_solarSystemName(system)))
	end
)
pricing_calculator = PricingCalculator.new(markets)
 
# Create the objects needed to compute material costs.
blueprint_repository = BlueprintRepository.new()
waste_calculator = WasteCalculator.new(5, blueprint_repository)
materials_calculator = MaterialsCalculator.new(waste_calculator)
 
# Set up our list of jobs
jobs = [
	["Warp Scrambler II", 100],
	["Expanded Cargohold II", 100]
].map { |name, count| Job.new(name, count) }
 
# Run our visitors to generate our build data.
build = Build.new("Initial Build", 1, jobs)
	.accept(materials_calculator)
	.accept(pricing_calculator)
 
# Print out what we've found.
ConsoleSerializer.new(build).write()

If you’ve been following along from the previous posts, you’ll notice that we’ve replaced our hard-coded system IDs with nicer ActiveRecord calls to find the market systems that we care about. Next we create a PricingCalculator visitor, which will use our pricing models to compute the cost and value of each build tree node.

We also create the objects needed to compute the materials for each node in the build tree. The formulas are somewhat cryptic and probably not super-interesting to most (please check the source code if you’d like to see the details of the computations). Note that our MaterialsCalculator object is also a visitor we will use to traverse the build tree.

We’ll declare our build data, visit it with both our materials calculator and our pricing data. Then finally, we’ll print our results to the console for now with a ConsoleSerializer object. We can work on a fancier presentation later.

Visiting and Iterating over Nodes

The build nodes inherit from a common root class, and the method that allows us to walk the tree with visitors is simply:

	def accept(visitor)
		clone.tap { |copy|
			copy.data = @data.clone
			copy.children = @children.map { |c| c.accept(visitor) }
			visitor.visit(copy)
		}
	end

We apply the visitor to a copy of the node and it’s children rather than modify the original (we’ll look at fancier ways to do this copying in the future). This allows us to apply different visitors to the same node and compare the results. Let’s say we wanted to see what the difference in costs would be with two different pricing models; applying one model would not change the initial data, and we will easily be able to apply another visitor and compare the results of each.

The same class implements the code to sort and traverse the nodes:

	def each(&block)
		@children.each { |c| c.each(&block) }
		yield self
	end
 
	def sort_by(&block)
		clone.tap { |copy|
			copy.data = @data.clone
			copy.children = @children.sort_by(&block)
		}
	end

Taking the &block argument allows a method to receive a block, and calling yield invokes the block with the given arguments. If you want the block to be optional, you can use the block_given? method to detect whether a block is available. You can also use block.arity to detect how many arguments the block expects. In our case, we could have thrown a custom exception if block_given? was false or block.arity did not equal 1. I’m okay with the default exception being thrown here.

Our ConsoleSerializer can now sort and iterate over our tree just doing something like:

	def write()
		@build.
			sort_by { |node| node.data[:profit_margin] }.
			each { |node| 
				# Do stuff for each node.
			}	
	end

One last note is that I’m formatting the currency values using the money gem. It makes converting a floating point value to the representation we want quite simple:

	def format_isk(value)
		Money.new(value * 100).format(:symbol => "ISK", :symbol_position => :after)
	end

Our Results

Running the script at the time of writing gives us some data like:

--------------------------------------------------
Warp Scrambler II - 100
--------------------------------------------------
Cost: 71,994,086.20 ISK
Value: 134,819,272.60 ISK
Profit: 62,825,186.40 ISK
Profit Margin: 87.26 %
Materials:
	100 Warp Scrambler I - per unit: 64,989.23 ISK - total: 6,498,922.60 ISK
	500 Transmitter - per unit: 8,017.79 ISK - total: 4,008,894.00 ISK
	1000 Miniature Electronics - per unit: 7,922.58 ISK - total: 7,922,580.00 ISK
	100 R.A.M.- Electronics - per unit: 71,397.12 ISK - total: 7,139,712.20 ISK
	200 Gravimetric Sensor Cluster - per unit: 28,899.42 ISK - total: 5,779,883.20 ISK
	600 Quantum Microprocessor - per unit: 53,949.17 ISK - total: 32,369,503.20 ISK
	35700 Tritanium - per unit: 4.80 ISK - total: 171,360.00 ISK
	12500 Pyerite - per unit: 11.44 ISK - total: 143,050.00 ISK
	23900 Mexallon - per unit: 44.60 ISK - total: 1,065,987.80 ISK
	6600 Isogen - per unit: 125.65 ISK - total: 829,276.80 ISK
	200 Zydrine - per unit: 726.21 ISK - total: 145,241.20 ISK
	800 Morphite - per unit: 7,399.59 ISK - total: 5,919,675.20 ISK
 
--------------------------------------------------
Expanded Cargohold II - 100
--------------------------------------------------
Cost: 17,767,344.60 ISK
Value: 49,998,979.60 ISK
Profit: 32,231,635.00 ISK
Profit Margin: 181.41 %
Materials:
	100 Expanded Cargohold I - per unit: 2,728.50 ISK - total: 272,850.00 ISK
	500 Construction Blocks - per unit: 8,246.70 ISK - total: 4,123,349.00 ISK
	100 R.A.M.- Armor/Hull Tech - per unit: 61,319.39 ISK - total: 6,131,939.40 ISK
	100 Nanomechanical Microprocessor - per unit: 47,174.39 ISK - total: 4,717,438.60 ISK
	100 Crystalline Carbonide Armor Plate - per unit: 8,388.78 ISK - total: 838,877.60 ISK
	500 Isogen - per unit: 125.65 ISK - total: 62,824.00 ISK
	200 Nocxium - per unit: 700.74 ISK - total: 140,147.20 ISK
	200 Morphite - per unit: 7,399.59 ISK - total: 1,479,918.80 ISK
 
--------------------------------------------------
Initial Build - 1
--------------------------------------------------
Cost: 89,761,430.80 ISK
Value: 184,818,252.20 ISK
Profit: 95,056,821.40 ISK
Profit Margin: 105.9 %
Materials:
	100 Warp Scrambler I - per unit: 64,989.23 ISK - total: 6,498,922.60 ISK
	500 Transmitter - per unit: 8,017.79 ISK - total: 4,008,894.00 ISK
	1000 Miniature Electronics - per unit: 7,922.58 ISK - total: 7,922,580.00 ISK
	100 R.A.M.- Electronics - per unit: 71,397.12 ISK - total: 7,139,712.20 ISK
	200 Gravimetric Sensor Cluster - per unit: 28,899.42 ISK - total: 5,779,883.20 ISK
	600 Quantum Microprocessor - per unit: 53,949.17 ISK - total: 32,369,503.20 ISK
	35700 Tritanium - per unit: 4.80 ISK - total: 171,360.00 ISK
	12500 Pyerite - per unit: 11.44 ISK - total: 143,050.00 ISK
	23900 Mexallon - per unit: 44.60 ISK - total: 1,065,987.80 ISK
	7100 Isogen - per unit: 125.65 ISK - total: 892,100.80 ISK
	200 Zydrine - per unit: 726.21 ISK - total: 145,241.20 ISK
	1000 Morphite - per unit: 7,399.59 ISK - total: 7,399,594.00 ISK
	100 Expanded Cargohold I - per unit: 2,728.50 ISK - total: 272,850.00 ISK
	500 Construction Blocks - per unit: 8,246.70 ISK - total: 4,123,349.00 ISK
	100 R.A.M.- Armor/Hull Tech - per unit: 61,319.39 ISK - total: 6,131,939.40 ISK
	100 Nanomechanical Microprocessor - per unit: 47,174.39 ISK - total: 4,717,438.60 ISK
	100 Crystalline Carbonide Armor Plate - per unit: 8,388.78 ISK - total: 838,877.60 ISK
	200 Nocxium - per unit: 700.74 ISK - total: 140,147.20 ISK

So what have we accomplished? Well, in this particular case, we’ve established that building “Expanded Cargohold II” modules yields a greater profit margin than “Warp Scrambler II”s. You can easily imagine running this same comparison on all T2 frigates. Or you know, every buildable item in the game. Note that at this point the calculations do not include invention costs — we’ll add those later with another visitor.

Next time we’ll look at building shopping lists for our required materials, sorting by item type and by preferred market. Happy manufacturing!


Hacking EVE