Shipyard generation
Posted: Tue Dec 30, 2014 5:27 pm
It got me thinking, so I hacked together an alternative way of generating shipyards. Simple demonstrator code below (sorry I can't find a way of making it smaller on the screen), fwiw.[url=https://bb.oolite.space/viewtopic.php?p=231204#p231204]in another thread[/url], cim wrote:In theory that random value was itself reseeded every 90ish days, but there was an error in that code so it was in effect a constant. With changes elsewhere over the last few versions that seed value is now only used by the shipyard, so it's been replaced with the constant for clarity until someone gets round to getting rid of it entirely and rewriting the shipyard generation to be something more transparent.jh145 wrote:I can't tell how that "random" value is (re)seeded
If this comes at the wrong time, in the wrong language, on the wrong forum, that's okay; I'm here to learn.
Code: Select all
#!/usr/bin/env python
#
# Demonstration of simple no-state shipyard generation.
#
# This shows one way to generate shipyards in a reasonably robust manner
# with no state saved to disk. The shipyard generation function just
# needs a shipyard id, the time, and a list of ships allowed to be sold
# in the yard (or we could change it to take a list of all ships in the
# game and let the yard filter by tech level etc). Each ship in the
# list needs two fields: guid and chance (and, for the demo, name).
# The shipyard may optionally support a non-standard number of sales
# slots, sales rate, and replacement efficiency.
#
# The shipyard evolves gradually over time, which I hope is considered
# an improvement over the current all-or-nothing update behaviour.
from hashlib import md5
__all__ = ['shipyard'] ### API
# -------------------------------------------------------------------------
# defaults for the shipyard() function call
# -------------------------------------------------------------------------
DEFAULT_NUMSLOTS = 20 ### up to 20 ships for sale
DEFAULT_CYCLE = 90*86400 ### inventory lasts ~90 days
DEFAULT_EFFICIENCY = 0.8 ### typically ~80% stocked
# -------------------------------------------------------------------------
# utility functions
# -------------------------------------------------------------------------
def B128(string):
return int(md5(string).hexdigest(),16)
def U01(string):
return B128(string) / 2.0**128
# -------------------------------------------------------------------------
# what ship is in which slot at what time?
# -------------------------------------------------------------------------
def ship_in_slot(
yard_id,
ship_catalogue,
when,
yard_slotidx,
yard_numslots,
yard_cycle,
yard_efficiency
):
# catch various degenerate conditions
#
if yard_numslots==0 or yard_efficiency==0 or not ship_catalogue:
return None
assert(0<=yard_slotidx<yard_numslots)
assert(0<yard_efficiency<=1)
assert(yard_cycle>0)
# 1/slot_cycle = x/yard_cycle for x in [0.5,1.5]
# then use that to quantise time
#
if yard_numslots == 1:
x = 1
else:
x = 0.5 + yard_slotidx/(yard_numslots-1.0)
cycle_nr = int(when*x/yard_cycle)
# seed the PRNG on this shipyard, this slot, this cycle nr
#
tag = 'yard=%s,slot=%s,cycnr=%s' % (yard_id,yard_slotidx,cycle_nr)
# choose a ship, but reorder first to give a little
# robustness in the face of catalogue changes
#
ship_catalogue = sorted(
ship_catalogue,
key=lambda s:md5('catalogue[%s,ship=%s]'%(tag,s.guid)).digest()
)
prtot = sum(ship.chance for ship in ship_catalogue)
ship_p = U01('ship[%s]'%tag) * prtot/yard_efficiency
prsum = 0
for ship in ship_catalogue:
prsum += ship.chance
if prsum > ship_p:
break
else:
return None ### inefficient yard hasn't restocked
# choose equipment
# -- for the demo, just give a raw bit vector
#
eq_bits = B128('equipment[%s,ship=%s]'%(tag,ship.guid))
equipment = eq_bits
# we're done
#
return ship,equipment
# -------------------------------------------------------------------------
# calculate a whole shipyard's contents
# (simplified to return a printable string for this demo)
# -------------------------------------------------------------------------
def shipyard(
yard_id,
ship_catalogue,
when,
yard_numslots=DEFAULT_NUMSLOTS,
yard_cycle=DEFAULT_CYCLE,
yard_efficiency=DEFAULT_EFFICIENCY
):
def printable_slot(s):
name,equipment = (s[0].name,s[1]) if s else ('---',0)
return '%20s (eq=%032x)' % (name,equipment)
return [
printable_slot( ship_in_slot(
yard_id,
ship_catalogue,
when,
yard_slotidx,
yard_numslots,
yard_cycle,
yard_efficiency
) ) for yard_slotidx in range(yard_numslots)
]
# -------------------------------------------------------------------------
# simple demo
# -------------------------------------------------------------------------
def demo():
class Ship:
def __init__(self,guid,name,chance):
self.guid = guid
self.name = name
self.chance = chance
# don't bother emulating anything else for this simple demo
DAY_BEFORE = 50
DAY_AFTER = 90
SHIPYARD_ID = 'Honest Jameson\'s'
PV_RARE = Ship('Pit Viper','Pit Viper',0.1)
PV_COMMON = Ship('Pit Viper','Pit Viper',0.5)
def show(title,left,right):
print
print title
print
for l,r in zip(left,right):
if l == r:
print l, ' ', r
elif '--- (' in l:
print l, ' > ', r
elif '--- (' in r:
print l, ' < ', r
else:
print l, ' | ', r
# you must pass a catalogue of ships suitable for this station
# i.e. caller does filtering by tech level etc.
#
ship_catalogue = [Ship(name,name,chance) for name,chance in (
( 'Cobra Mk III', 1 ),
( 'Fer-de-Lance', 0.9 ),
( 'Python', 1 ),
( 'Python ET Special', 0.6 ),
( 'Wolf Mk I', 0.7 ),
)]
ship_catalogue.append(PV_RARE)
# Normal behaviour: some ships sell (and perhaps get replaced),
# others don't. Later slots update more quickly.
#
before = shipyard(SHIPYARD_ID,ship_catalogue,DAY_BEFORE*86400)
after = shipyard(SHIPYARD_ID,ship_catalogue,DAY_AFTER*86400)
show(
'Day %d, then %d days later ...' % (DAY_BEFORE,DAY_AFTER-DAY_BEFORE),
before, after
)
# Introducing new ships to the game causes some shipyard slots
# to be given over to the new ships, but will also cause some
# replacement of old ships with different old ships.
#
ship_catalogue += [Ship(name,name,chance) for name,chance in (
( '*** OXP ship 1 ***', 0.8 ),
( '*** OXP ship 2 ***', 0.8 ),
)]
reloaded = shipyard(SHIPYARD_ID,ship_catalogue,DAY_AFTER*86400)
show(
'Reloading day %d with two new OXP ships ...' % DAY_AFTER,
after, reloaded
)
# Changing the chance of a ship has a similar effect to the
# introduction of new ships.
#
ship_catalogue = [s for s in ship_catalogue if s.guid!=PV_RARE.guid]
ship_catalogue.append(PV_COMMON)
morevipers = shipyard(SHIPYARD_ID,ship_catalogue,DAY_AFTER*86400)
show(
'Making the Pit Viper more probable ...',
reloaded, morevipers
)
print
# -------------------------------------------------------------------------
# CLI
# -------------------------------------------------------------------------
if __name__ == '__main__':
demo()