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()