starquest.py

Created by laigna

Created on March 14, 2026

14.8 KB

Star Quest — DnD-style space RPG. Captain the starship Artemis across procedurally generated star systems. 4 classes, 16 events, stat-based skill checks, resource management, 6 unique endings. Inspired by Expanse, Dune, BSG and Foundation.


# Star Quest - NumWorks
# Created by Alvar Laigna - https://alvarlaigna.com
# Arrows:choose OK:select Back:quit
from kandinsky import fill_rect as F,draw_string as D
from ion import keydown as K
from time import sleep as Z,monotonic as M
from random import randint as RI,choice as RC
SW,SH=320,222
BK=(0,)*3;WH=(255,)*3
TL=(0,200,200);GY=(120,)*3
RD=(220,40,40);GN=(40,200,40)
YL=(220,200,40);BL=(40,80,220)
OR=(220,140,40);MG=(180,40,180)
okv=1 if K(4)or K(52)else 0
def okd():return K(4)or K(52)
def okp():
 global okv
 d=okd()
 if d and not okv:okv=1;return True
 if not d:okv=0
 return False
def wup():
 while okd():Z(0.02)
 Z(0.12)
# stars cache
STS=None
def mkst():
 global STS
 STS=[(RI(0,319),RI(0,221),RC([WH,GY,(180,)*3]))for _ in range(45)]
def stars():
 F(0,0,SW,SH,BK)
 for x,y,c in STS:F(x,y,RI(1,2),RI(1,2),c)
def sbar(fu,hu,cr,cw,cl):
 F(0,0,SW,14,BK)
 cs=[(fu,"F",RD if fu<25 else YL if fu<50 else GN),
  (hu,"H",RD if hu<25 else YL if hu<50 else GN),
  (cr,"$",YL),(cw,"C",OR),(cl,"*",TL)]
 x=2
 for v,lb,co in cs:
  D(lb+str(v),x,0,co,BK);x+=62
def wrap(t,x,y,fg,bg,w=30):
 ws=t.split(' ');ln="";ly=y
 for wd in ws:
  if len(ln)+len(wd)+1>w:
   D(ln,x,ly,fg,bg);ly+=18;ln=wd
  else:ln=(ln+' '+wd).strip()
 if ln:D(ln,x,ly,fg,bg);ly+=18
 return ly
def tbox(lines,y0=30):
 stars()
 ly=y0
 for ln in lines:ly=wrap(ln,8,ly,WH,BK)+4
 D("[ OK ]",130,200,TL,BK)
 wup()
 while not okp():Z(0.03)
 wup()
def menu(opts,y0=100,tl=""):
 sel=0;n=len(opts)
 while True:
  if tl:stars();D(tl,8,30,TL,BK)
  else:stars()
  for i,o in enumerate(opts):
   fg=TL if i==sel else GY
   D((">" if i==sel else " ")+o,20,y0+i*22,fg,BK)
  D("Arrows+OK",110,200,GY,BK)
  wup()
  while True:
   if K(1)and sel>0:sel-=1;break
   if K(2)and sel<n-1:sel+=1;break
   if okp():wup();return sel
   if K(17):return-1
   Z(0.03)
  Z(0.12)
# ship drawing
def dship(x,y,c):
 F(x+12,y,4,16,c);F(x+8,y+4,12,8,c)
 F(x+4,y+6,4,4,c);F(x,y+7,4,2,c)
 F(x+16,y+2,4,4,c);F(x+16,y+10,4,4,c)
# combat anim
def cbanim():
 stars()
 dship(60,90,TL);dship(220,90,RD)
 for i in range(4):
  co=YL if i%2==0 else RD
  F(80,96+RI(-3,3),140,2,co)
  Z(0.15)
  F(80,90,140,16,BK)
  for x,y,c in STS:
   if 80<=x<=220 and 85<=y<=110:F(x,y,2,2,c)
# clue anim
def clanim():
 cx,cy=160,111
 for r in range(5,60,6):
  co=RC([TL,BL,WH,(0,180,255)])
  F(cx-r,cy-r,r*2,2,co);F(cx-r,cy+r,r*2,2,co)
  F(cx-r,cy-r,2,r*2,co);F(cx+r,cy-r,2,r*2,co)
  Z(0.06)
 Z(0.4)
# death anim
def deanim():
 for _ in range(20):
  F(RI(100,220),RI(60,160),RI(4,20),RI(4,20),RC([RD,OR,YL]))
  Z(0.08)
 Z(0.5)
# classes: name,pilot,combat,diplo,tech
CLS=[("Ace Pilot",4,2,2,2),("War Veteran",2,4,2,2),
 ("Ambassador",2,2,4,2),("Technician",2,2,2,4)]
CDESC=["Born in the cockpit.","Forged in fire.",
 "Words are weapons.","Machines speak to me."]
# location gen
LADJ=["Burning","Frozen","Shattered","Hidden",
 "Ancient","Dying","Rogue","Drifting","Crimson","Silent"]
LNOU=["Station","Colony","Outpost","Nebula",
 "Asteroid","Wreckage","Gate","Moon","Beacon","Rift"]
def locn():return RC(LADJ)+" "+RC(LNOU)
# Events: (narrative, choices)
# choice: (label,stat_idx,diff,sok,fok,stxt,ftxt)
# stat_idx: 0=pil,1=cmb,2=dip,3=tec,-1=none
# sok/fok: (fuel,hull,cred,crew,clue)
# When stat_idx=-1, sok always applies
EVT=[
("Distress beacon. A cargo hauler|bleeds air, raiders circling|like wolves around a fire.",[
 ("Fight",1,6,(0,0,20,1,0),(-5,-15,0,0,0),"Raiders scatter. Survivors|join your crew.","A brutal exchange. Hull|screams under cannon fire."),
 ("Negotiate",2,7,(0,0,10,0,0),(0,0,-10,0,0),"Your words cut sharper|than any blade. They leave.","They take a tithe. The|price of failed words."),
 ("Flee",0,5,(0,0,0,0,0),(-10,-5,0,0,0),"The Artemis dances through|the crossfire untouched.","Stray rounds find you|as you burn hard away.")]),
("An ancient probe tumbles|through starlight, its hull|etched with impossible geometry.",[
 ("Scan",3,8,(0,0,0,0,1),(0,-10,0,0,0),"Data streams flood your|screens. A Nexus fragment!","The probe detonates. Its|secrets die in fire."),
 ("Tow it",0,6,(0,0,30,0,0),(-15,0,0,0,0),"Collectors pay handsomely|for alien artifacts.","The mass nearly drains|your fuel reserves dry."),
 ("Leave it",-1,0,(0,0,0,0,0),(0,0,0,0,0),"Some mysteries are best|left in the dark.","")]),
("A Dominion dreadnought fills|your viewport. Cold voice:|'Surrender cargo and crew.'",[
 ("Resist",1,7,(0,0,15,0,0),(0,-25,0,-3,0),"Your volleys find their|reactor. A brilliant death.","Their guns rake your|hull. Crew lost to void."),
 ("Talk down",2,6,(0,0,0,0,0),(0,0,-20,0,0),"You invoke an old code.|They withdraw, grudging.","They take what they want.|You survive. Barely."),
 ("Outrun",0,6,(0,0,0,0,0),(-20,0,0,0,0),"Full burn. They eat your|exhaust trail. Freedom.","The chase bleeds your|fuel tanks nearly dry.")]),
("Syndicate boss, chrome teeth|gleaming: 'Sabotage the colony|shields. I pay well.'",[
 ("Accept",3,7,(0,0,40,0,0),(0,-15,0,0,0),"Clean work. They never|knew you were there.","Colony guards catch you.|Your ship takes the heat."),
 ("Refuse",-1,0,(0,-10,0,0,0),(0,0,0,0,0),"They open fire as you|leave. A principled scar.",""),
 ("Double-cross",1,7,(0,0,25,0,0),(0,-20,0,-2,0),"You save the colony and|raid the Syndicate vault.","A messy fight. The boss|nearly ends you both.")]),
("Nebula storm. Electromagnetic|waves crash like ocean surf|against your failing shields.",[
 ("Ride it out",0,6,(0,0,0,0,0),(0,-15,-10,0,0),"You thread the needle|between lightning walls.","Systems overload. Repairs|will cost dearly."),
 ("Reroute power",3,8,(0,0,0,0,1),(0,-10,0,0,0),"Sensors pierce the storm.|A Nexus signal! Captured.","Power surge. Conduits|blow across three decks.")]),
("Refugee fleet, hundreds of|lives packed into rusting|shells. They beg for help.",[
 ("Give supplies",-1,0,(-15,0,-10,2,0),(0,0,0,0,0),"Grateful volunteers join.|Their hope becomes yours.",""),
 ("Trade info",2,7,(0,0,0,0,1),(0,0,-5,0,0),"Star maps from the old|worlds. A Nexus bearing!","They have little. You|share fuel as goodwill."),
 ("Pass by",-1,0,(0,0,0,-1,0),(0,0,0,0,0),"You leave them behind.|A crewmember deserts in|silent protest.","")]),
("Derelict warship hangs in|the void. Dark windows like|dead eyes watching you.",[
 ("Board it",1,6,(0,0,25,0,0),(0,-15,0,-2,0),"Gold in the hold.|Whoever died here died|rich.","Ambush! Pirates nested|in the corpse of a ship."),
 ("Scan",3,7,(0,0,0,0,1),(0,0,0,0,0),"Its black box holds|coordinates. Nexus data!","Interference. The dead|keep their secrets."),
 ("Salvage hull",0,6,(0,20,0,0,0),(0,-10,0,0,0),"Patch plates for your|wounded hull. Good haul.","Debris tumbles wrong.|More damage than repair.")]),
("Wormhole shimmers like a|tear in reality. Faint|energy readings from within.",[
 ("Enter",0,8,(0,0,0,0,1),(-20,-10,0,0,0),"You emerge near a Nexus|beacon! Data captured!","The passage is violent.|You barely survive it."),
 ("Probe it",3,6,(0,0,15,0,0),(0,0,0,0,0),"Sensor data sells well|to the Science Guild.","The probe vanishes.|Nothing gained."),
 ("Avoid",-1,0,(0,0,0,0,0),(0,0,0,0,0),"Caution keeps you alive|another day.","")]),
("Seeker mystic hails you.|Robed in starlight, voice|like grinding planets.",[
 ("Listen",2,7,(0,0,0,0,1),(0,0,-15,0,0),"Cryptic words but real|coordinates. A Nexus clue!","Riddles and demands.|You pay for nonsense."),
 ("Trade tech",3,6,(0,0,10,0,0),(0,0,-10,0,0),"Your engineering impresses.|Fair exchange of knowledge.","They scoff at your crude|machines. Insult fee paid."),
 ("Ignore",-1,0,(0,-5,0,0,0),(0,0,0,0,0),"They curse your ship as|you pass. Systems glitch.","")]),
("Fuel depot. The owner|squints at you: 'Cash up|front. No charity here.'",[
 ("Buy fuel",-1,0,(-10,0,-15,0,0),(0,0,0,0,0),"Wait -- you're PAYING|fuel? No, you GAIN fuel.",""),
 ("Haggle",2,6,(20,0,-5,0,0),(10,0,-10,0,0),"Sweet-talked into a bulk|discount. Tanks topped off.","Marginal deal. Better|than nothing out here."),
 ("Rob them",1,7,(30,0,0,0,0),(0,-10,0,-1,0),"Quick and brutal. Full|tanks, clear conscience.","Security drones bite|back. A costly mistake.")]),
("Colony under siege! Walls|crumbling as Dominion shells|rain from orbit.",[
 ("Defend",1,8,(0,-10,0,2,1),(0,-20,0,-2,0),"You turn the tide! In the|rubble, a Nexus artifact!","Overwhelming fire. You|rescue who you can."),
 ("Evacuate",0,6,(0,0,0,1,0),(-10,0,0,0,0),"You save dozens. A pilot's|duty, well performed.","Fuel burns hot in the|frantic escape runs."),
 ("Bypass",2,6,(0,0,10,0,0),(0,0,0,0,0),"You broker a pause. Both|sides pay for the service.","Your words fall on deaf|cannons. You move on.")]),
("Black market bazaar hidden|inside a hollowed moon.|Everything has a price.",[
 ("Shop",-1,0,(0,15,-20,0,0),(0,0,0,0,0),"Hull patches and stim|packs. Expensive but real.",""),
 ("Sell intel",2,7,(0,0,35,0,0),(0,0,0,-1,0),"Your star charts fetch a|premium from desperate men.","Wrong buyer. They take|a crewmember as leverage."),
 ("Hack vault",3,8,(0,0,40,0,1),(0,-15,0,-1,0),"Jackpot. Credits and an|encrypted Nexus cipher!","Security locks bite.|Costly escape ensues.")]),
("Solar flare incoming.|Your sensors scream warnings|as the star convulses.",[
 ("Shield up",3,6,(0,0,0,0,0),(0,-20,0,0,0),"Shields hold. You watch|a star's fury, humbled.","Shields fail. The ship|screams in the light."),
 ("Ride the wave",0,8,(0,0,0,0,1),(-15,-10,0,0,0),"You surf the wavefront!|Strange signals in the|plasma -- Nexus data!","Battered through the|inferno. Ship limps on."),
 ("Take cover",0,5,(0,0,0,0,0),(0,-10,-5,0,0),"Asteroid shadow saves|you from the worst.","Glancing blow. Could|have been much worse.")]),
("Alien vessel. No known|configuration. It pulses|with bioluminescent light.",[
 ("Hail them",2,8,(0,0,0,1,1),(0,0,0,0,0),"First contact! They share|ancient Nexus knowledge!","Silence. They drift on,|unknowable and vast."),
 ("Scan",3,6,(0,0,20,0,0),(0,-5,0,0,0),"Bio-tech readings worth|a fortune to scholars.","A defensive pulse. Your|sensors overload briefly."),
 ("Follow",0,6,(0,0,0,0,0),(-15,0,0,0,0),"They lead you to a safe|harbor. A rare kindness.","They outpace you easily.|Fuel wasted in pursuit.")]),
("Mutiny brewing. Your crew|whispers in dark corners|about turning back.",[
 ("Speech",2,6,(0,0,0,1,0),(0,0,0,-2,0),"Your words rekindle fire|in their hearts. Onward.","Two leave in the escape|pod. The rest stay quiet."),
 ("Show strength",1,5,(0,0,0,0,0),(0,0,0,-1,0),"They remember why you're|captain. Order restored.","One makes an example of|themselves. Regrettable."),
 ("Compromise",3,5,(0,0,0,0,0),(0,0,-10,0,0),"Better rations from the|replicator. Morale climbs.","It costs more resources|but the grumbling fades.")]),
("Ancient ruins on a dead|moon. Towers of crystal|sing in frequencies of|forgotten gods.",[
 ("Explore",1,6,(0,0,15,0,0),(0,-10,0,-1,0),"Relics worth a fortune.|Crystal sings for you.","Cave-in. You dig out|but not everyone makes it."),
 ("Study",3,8,(0,0,0,0,1),(0,0,0,0,0),"The crystals encode star|maps. A Nexus waypoint!","Beautiful but opaque.|The data resists you."),
 ("Mine crystals",-1,0,(0,0,20,0,0),(0,0,0,0,0),"Raw crystal fetches good|prices at any port.","")]),
]
# fix fuel depot event sok
def fixevt():
 e=EVT[9];c=list(e[1]);old=c[0]
 c[0]=("Buy fuel",-1,0,(15,0,-15,0,0),(0,0,0,0,0),old[5],old[6])
 EVT[9]=(e[0],c)
# endings
EDIE=["The Artemis drifts, engines|cold and dark. Stars watch|in silence as the void|claims another dreamer.",
 "Hull breach. Atmosphere|screams into nothing. The|void takes what it is|owed without mercy.",
 "Alone on an empty ship.|The last captain of the|Artemis stares into the|endless, uncaring dark."]
ENEX=[
 "The Nexus pulses before you,|a sphere of living light|older than the stars.|Three paths. One choice.",
 "You share the Nexus.|Its light spreads across|the galaxy like dawn.|Factions lay down arms.|The Accord reborn, not|as law but as hope.|They call you the|Lightbringer.",
 "You claim the Nexus.|Power floods your veins.|Fleets kneel. Worlds bow.|Emperor of starlight.|But in your dreams, the|Nexus whispers: even|gods eventually fall.",
 "You destroy the Nexus.|The explosion is silent|and beautiful. Every soul|in the galaxy gasps as|chains they never knew|they wore dissolve.|You vanish into legend.|Free. Finally, truly free."]
def run():
 fixevt()
 mkst()
 while True:
  # title
  stars()
  dship(55,85,TL);dship(60,110,GY)
  F(100,50,180,4,TL);F(100,130,180,4,TL)
  D("STAR QUEST",105,70,TL,BK)
  D("A Space Odyssey",100,95,GY,BK)
  D("Press OK",125,180,WH,BK)
  D("BACK to quit",110,200,GY,BK)
  wup()
  while True:
   if okp():break
   if K(17):return
   Z(0.03)
  wup()
  # class select
  ci=menu(["Pilot - cockpit born","Veteran - forged in fire",
   "Ambassador - word-smith","Technician - machine kin"],60,"Choose your path:")
  if ci<0:continue
  _,pi,co,di,te=CLS[ci]
  st=[pi,co,di,te]
  tbox([CDESC[ci],"","Pilot:%d Combat:%d"%(pi,co),"Diplo:%d Tech:%d"%(di,te)])
  fu=80;hu=80;cr=30;cw=5;cl=0;turn=0
  # main loop
  alive=True
  while alive and cl<5:
   turn+=1
   loc=locn()
   ev=RC(EVT)
   nar=ev[0].split("|")
   chs=ev[1]
   # show location + narrative
   stars()
   sbar(fu,hu,cr,cw,cl)
   D(loc,8,18,TL,BK)
   ly=40
   for ln in nar:ly=wrap(ln,8,ly,WH,BK)+2
   D("[ OK ]",130,200,TL,BK)
   wup()
   while not okp():Z(0.03)
   wup()
   # show choices
   labs=[c[0]for c in chs]
   si=menu(labs,100,"What do you do?")
   if si<0:si=len(chs)-1
   ch=chs[si]
   lb,sx,df,sok,fok,stxt,ftxt=ch
   # resolve
   if sx>=0 and df>0:
    roll=st[sx]+RI(1,6)
    suc=roll>=df
   else:
    suc=True
   # combat anim for combat checks
   if sx==1 and df>0:cbanim()
   res=sok if suc else fok
   dfu,dhu,dcr,dcw,dcl=res
   fu+=dfu;hu+=dhu;cr+=dcr;cw+=dcw;cl+=dcl
   fu=min(fu,100);hu=min(hu,100)
   # clue anim
   if dcl>0 and suc:clanim()
   # result text
   rtxt=stxt if suc else ftxt
   if rtxt:
    hdr="SUCCESS" if suc else "FAILURE"
    hco=GN if suc else RD
    stars()
    sbar(fu,hu,cr,cw,cl)
    D(hdr,120,30,hco,BK)
    ly=55
    for ln in rtxt.split("|"):ly=wrap(ln,8,ly,WH,BK)+2
    # show changes
    ch_t=""
    if dfu:ch_t+="Fuel%+d "%dfu
    if dhu:ch_t+="Hull%+d "%dhu
    if dcr:ch_t+="Cred%+d "%dcr
    if dcw:ch_t+="Crew%+d "%dcw
    if dcl:ch_t+="CLUE! "
    if ch_t:D(ch_t.strip(),8,ly+10,YL,BK)
    D("[ OK ]",130,200,TL,BK)
    wup()
    while not okp():Z(0.03)
    wup()
   # check death
   if fu<=0:
    stars();deanim()
    tbox(EDIE[0].split("|"));alive=False
   elif hu<=0:
    stars();deanim()
    tbox(EDIE[1].split("|"));alive=False
   elif cw<=0:
    stars();deanim()
    tbox(EDIE[2].split("|"));alive=False
   # fuel drain per turn
   if alive:fu=max(0,fu-RI(2,5))
  # nexus ending
  if cl>=5:
   tbox(ENEX[0].split("|"))
   ei=menu(["Share the Nexus","Claim the Nexus","Destroy the Nexus"],80,
    "The galaxy holds its breath:")
   if ei<0:ei=0
   # nexus visual
   stars()
   cx,cy=160,100
   cs=[TL,WH,(255,215,0)]
   for r in range(5,80,4):
    F(cx-r,cy-r,r*2,r*2,cs[r%3])
    Z(0.04)
   Z(0.5)
   tbox(ENEX[ei+1].split("|"))
   stars()
   D("Turns survived: %d"%turn,60,80,TL,BK)
   D("Final score",100,110,WH,BK)
   sc=turn*10+cr+cw*20
   D(str(sc),140,140,YL,BK)
   D("[ OK ]",130,200,TL,BK)
   wup()
   while not okp():Z(0.03)
   wup()
run()

During your visit to our site, NumWorks needs to install "cookies" or use other technologies to collect data about you in order to:

With the exception of Cookies essential to the operation of the site, NumWorks leaves you the choice: you can accept Cookies for audience measurement by clicking on the "Accept and continue" button, or refuse these Cookies by clicking on the "Continue without accepting" button or by continuing your browsing. You can update your choice at any time by clicking on the link "Manage my cookies" at the bottom of the page. For more information, please consult our cookies policy.