Build Capital Quest for CoCo 3 with maps and emulator tests
This commit is contained in:
commit
9f7d6f4786
44 files changed
+6238
No files matched your search
@@ -0,0 +1,80 @@
|
||||
// autostart.c - Program to be run by the Disk Basic DOS command.
|
||||
// By Pierre Sarrazin <http://sarrazip.com/>
|
||||
// This file is in the public domain.
|
||||
//
|
||||
// Reference: Roger Schrag, "A Special Use For The DOS Command",
|
||||
// The Rainbow, November 1984, p.140.
|
||||
//
|
||||
// To compile: cmoc --dos autostart.c
|
||||
// To install on CoCo disk image: install-coco-boot-loader some-image.dsk autostart.bin
|
||||
// To run on CoCo: type DOS and <Enter> to automatically run *.BAS.
|
||||
//
|
||||
// Version history:
|
||||
// 0.1.0 2016-01-16 Use of legacy.h discontinued.
|
||||
// 0.1.1 2018-04-03 Adapted to CMOC 0.1.51 (const).
|
||||
// 0.1.2 2018-08-04 Basic line now forced to be uppercase.
|
||||
|
||||
#include <coco.h>
|
||||
|
||||
#define ENDFLG 0x00 // STOP/END FLAG: POSITIVE=STOP, NEG=END
|
||||
#define CINBFL 0x70 // CONSOLE IN BUFFER FLAG: 00=NOT EMPTY, $FF=EMPTY
|
||||
#define RVEC4 0x016a // RAM hook: CONSOLE IN
|
||||
|
||||
|
||||
byte oldHookInstr;
|
||||
word oldHookAddr;
|
||||
const char *basicLineReader;
|
||||
|
||||
|
||||
void feedKeyboard()
|
||||
{
|
||||
word preservedX;
|
||||
asm("stx", preservedX);
|
||||
|
||||
* (byte *) CINBFL = 0; // console in buffer now not empty
|
||||
|
||||
char nextBasicByte = (char) toupper(*basicLineReader);
|
||||
++basicLineReader;
|
||||
if (nextBasicByte == 0) // if finished returning Basic line
|
||||
{
|
||||
* (byte *) RVEC4 = oldHookInstr;
|
||||
* (word *) (RVEC4 + 1) = oldHookAddr;
|
||||
nextBasicByte = '\r'; // return "Enter" to cause execution of Basic line
|
||||
}
|
||||
|
||||
asm {
|
||||
lda nextBasicByte
|
||||
ldx preservedX
|
||||
tfr u,s // discard stack frame
|
||||
leas 4,s // discard previous stack frame ptr and return addr
|
||||
rts // return to caller of CONSOLE IN
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
* (byte *) ENDFLG = - * (byte *) ENDFLG; // why?
|
||||
|
||||
basicLineReader =
|
||||
#ifndef COMMAND
|
||||
"LOADM\"CAPITAL.BIN\":EXEC"
|
||||
#else
|
||||
COMMAND
|
||||
#endif
|
||||
;
|
||||
|
||||
// Install function feedKeyboard() as CONSOLE IN routine.
|
||||
//
|
||||
oldHookInstr = * (byte *) RVEC4;
|
||||
oldHookAddr = * (word *) (RVEC4 + 1);
|
||||
|
||||
* (byte *) RVEC4 = 0x7e; // JMP instruction
|
||||
* (void **) (RVEC4 + 1) = (void *) feedKeyboard;
|
||||
|
||||
* (word *) 0x2600 = 0; // overwrite "OS" marker with two null bytes
|
||||
|
||||
asm("jmp", "$ac7c"); // go interpret a Basic line (which is read from console)
|
||||
|
||||
return 0; // avoids compiler error
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline, standard-library-only conversion of Fla-shop SVG to identity spans.
|
||||
SVG input uses straight M/L/H/V/Z paths. Flood regions at original resolution,
|
||||
associate via documented seed coordinates, then reduce to guest scanline runs.
|
||||
"""
|
||||
import collections, json, math, re, struct, zlib
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
W,H=2000,1200
|
||||
rows=[s.split('\t') for s in (ROOT/'data/states.tsv').read_text().splitlines()]
|
||||
assert len(rows)==50 and len({r[0] for r in rows})==50
|
||||
ids={r[0]:i+1 for i,r in enumerate(rows)}
|
||||
|
||||
def paths(d):
|
||||
tok=re.findall(r'[MmLlHhVvZz]|[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?',d)
|
||||
i=0;x=y=0;out=[];poly=[];cmd=''
|
||||
while i<len(tok):
|
||||
if tok[i].isalpha():
|
||||
cmd=tok[i];i+=1
|
||||
if cmd.lower()=='z':
|
||||
if poly:poly.append(poly[0]);out.append(poly);poly=[]
|
||||
x,y=out[-1][0];continue
|
||||
rel=cmd.islower(); c=cmd.lower()
|
||||
if c in ('m','l'):
|
||||
a,b=map(float,tok[i:i+2]);i+=2
|
||||
x,y=(x+a,y+b) if rel else (a,b)
|
||||
if c=='m':
|
||||
if poly:out.append(poly)
|
||||
poly=[];cmd='l' if rel else 'L'
|
||||
elif c=='h':
|
||||
a=float(tok[i]);i+=1;x=x+a if rel else a
|
||||
elif c=='v':
|
||||
a=float(tok[i]);i+=1;y=y+a if rel else a
|
||||
else:raise ValueError(cmd)
|
||||
poly.append((x,y))
|
||||
if poly:out.append(poly)
|
||||
return out
|
||||
|
||||
def png(path,w,h,pixels):
|
||||
def chunk(tag,b):return struct.pack('>I',len(b))+tag+b+struct.pack('>I',zlib.crc32(tag+b)&0xffffffff)
|
||||
raw=b''.join(b'\0'+bytes(pixels[y*w*3:(y+1)*w*3]) for y in range(h))
|
||||
path.write_bytes(b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>IIBBBBB',w,h,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(raw))+chunk(b'IEND',b''))
|
||||
|
||||
def main():
|
||||
svg=ET.parse(ROOT/'assets/usa-source.svg').getroot()
|
||||
elements={e.get('id'):e for e in svg.iter() if e.get('id')}
|
||||
land=bytearray(W*H)
|
||||
outline=paths(elements['outline'].get('d'))
|
||||
# Even-odd scan conversion, coast and islands included.
|
||||
edges=[(a,b) for p in outline for a,b in zip(p,p[1:]+p[:1]) if a[1]!=b[1]]
|
||||
buckets=[[] for _ in range(H)]
|
||||
for a,b in edges:
|
||||
for y in range(max(0,math.ceil(min(a[1],b[1])-.5)),min(H,math.ceil(max(a[1],b[1])-.5))):
|
||||
buckets[y].append(a[0]+(y+.5-a[1])*(b[0]-a[0])/(b[1]-a[1]))
|
||||
for y,xx in enumerate(buckets):
|
||||
xx.sort()
|
||||
for a,b in zip(xx[::2],xx[1::2]):
|
||||
lo=max(0,math.ceil(a-.5));hi=min(W,math.ceil(b-.5));land[y*W+lo:y*W+hi]=b'\1'*(hi-lo)
|
||||
coast=bytearray(land)
|
||||
# Three-source-pixel boundary stroke closes subpixel joins before flood fill.
|
||||
for p in paths(elements['boundaries'].get('d')):
|
||||
for a,b in zip(p,p[1:]):
|
||||
n=max(1,math.ceil(max(abs(a[0]-b[0]),abs(a[1]-b[1]))))
|
||||
for t in range(n+1):
|
||||
x=round(a[0]+(b[0]-a[0])*t/n);y=round(a[1]+(b[1]-a[1])*t/n)
|
||||
for yy in range(max(0,y-1),min(H,y+2)):
|
||||
for xx in range(max(0,x-1),min(W,x+2)):land[yy*W+xx]=0
|
||||
seeds={}
|
||||
for e in svg.iter():
|
||||
if e.tag.endswith('tspan'):
|
||||
ab=''.join(e.itertext()).strip()
|
||||
if ab in ids:seeds[ab]=[round(float(e.get('x'))+15),round(float(e.get('y'))-12)]
|
||||
# Callout labels are outside their states. Seed actual interiors instead.
|
||||
seeds.update({'VT':[1800,280],'NH':[1840,280],'MA':[1830,340],
|
||||
'RI':[1860,360],'CT':[1830,375],'NJ':[1780,440],
|
||||
'DE':[1764,509],'MD':[1720,536],'HI':[739,1130]})
|
||||
seeds.update(json.loads((ROOT/'data/map-seeds.json').read_text()) if (ROOT/'data/map-seeds.json').exists() else {})
|
||||
owners=bytearray(W*H);claimed={};counts={}
|
||||
for ab,(x,y) in seeds.items():
|
||||
start=y*W+x
|
||||
assert land[start],(ab,'seed is not land',x,y)
|
||||
|
||||
assert not owners[start],(ab,'shares component with',claimed.get(owners[start]))
|
||||
val=ids[ab];q=collections.deque([start]);owners[start]=val;count=0
|
||||
while q:
|
||||
pos=q.popleft();count+=1
|
||||
for nxt in (pos-1,pos+1,pos-W,pos+W):
|
||||
if 0<=nxt<W*H and land[nxt] and not owners[nxt]:owners[nxt]=val;q.append(nxt)
|
||||
claimed[val]=ab;counts[ab]=count
|
||||
# Unassigned land = detached islands/peninsulas: nearest assigned land,
|
||||
# using a multi-source breadth-first expansion. The source mask is retained.
|
||||
q=collections.deque(i for i in range(W*H) if owners[i] and any(0<=j<W*H and not owners[j] for j in (i-1,i+1,i-W,i+W)))
|
||||
nearest=bytearray(owners)
|
||||
while q:
|
||||
p=q.popleft()
|
||||
for n in (p-1,p+1,p-W,p+W):
|
||||
if 0<=n<W*H and not nearest[n]:nearest[n]=nearest[p];q.append(n)
|
||||
# Detached Alaska/Hawaii insets and Michigan's Upper Peninsula require
|
||||
# explicit geographic regions; proximity alone mislabels small islands.
|
||||
# Assign detached original land by geographic ownership, not nearest capital.
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
p=y*W+x
|
||||
if land[p] and not owners[p]:
|
||||
if 560<x<785 and 995<y<1190:owners[p]=ids['HI']
|
||||
elif x<560 and y>850:owners[p]=ids['AK']
|
||||
elif 1175<x<1465 and 180<y<330:owners[p]=ids['MI']
|
||||
else:owners[p]=nearest[p]
|
||||
(ROOT/'data/map-seeds.json').write_text(json.dumps(seeds,indent=2)+'\n')
|
||||
# Three views share the exact same owner raster and stable TSV IDs.
|
||||
views=[('national',200,110,(0,0,2000,1200)),('northeast',100,100,(1540,120,1980,600)),('hawaii',100,82,(565,995,785,1185))]
|
||||
arrays=[];meta={};preview=[]
|
||||
for name,w,h,(x0,y0,x1,y1) in views:
|
||||
small=[]
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
# Sample one source interior pixel at pixel center.
|
||||
sx=int(x0+(x+.5)*(x1-x0)/w);sy=int(y0+(y+.5)*(y1-y0)/h)
|
||||
small.append((owners[sy*W+sx] or nearest[sy*W+sx]) if coast[sy*W+sx] else 0)
|
||||
if name=='hawaii':small=[v if v==ids['HI'] else 0 for v in small]
|
||||
# One guest pixel on the right/bottom of a state seam gives continuous
|
||||
# boundaries. Preserve tiny national masks; their detail view is larger.
|
||||
areas=collections.Counter(small); original=small[:]
|
||||
for y in range(h-1):
|
||||
for x in range(w-1):
|
||||
pos=y*w+x;val=original[pos]
|
||||
if val and areas[val]>8 and any(v and v!=val for v in (original[pos+1],original[pos+w])):
|
||||
small[pos]=0
|
||||
spans=[];offsets=[0];total={}
|
||||
for val in range(1,51):
|
||||
cnt=0
|
||||
for y in range(h):
|
||||
x=0
|
||||
while x<w:
|
||||
if small[y*w+x]!=val:x+=1;continue
|
||||
first=x
|
||||
while x<w and small[y*w+x]==val:x+=1
|
||||
spans.extend([y,first,x-first]);cnt+=x-first
|
||||
offsets.append(len(spans));total[rows[val-1][0]]=cnt
|
||||
arrays.append('const unsigned char '+name+'_spans[] = {\n'+','.join(map(str,spans))+'\n};\nconst unsigned '+name+'_offsets[51] = {'+','.join(map(str,offsets))+'};\n')
|
||||
meta[name]={'size':[w,h],'source_box':[x0,y0,x1,y1],'pixels':total,'bytes':len(spans)+102}
|
||||
colors=[(8,19,34),(44,116,142),(73,145,155),(103,164,169),(36,92,126)]
|
||||
pix=[]
|
||||
for v in small:pix.extend(colors[0] if not v else colors[1+(v%4)])
|
||||
png(ROOT/'assets'/('preview-'+name+'.png'),w,h,pix)
|
||||
assert all(meta['national']['pixels'].values()),meta['national']['pixels']
|
||||
(ROOT/'src/map_data.c').write_text('/* Adapted from Fla-shop.com, CC BY 4.0. See assets/CREDITS.md. */\n#include "map_data.h"\n'+''.join(arrays))
|
||||
(ROOT/'include/map_data.h').write_text('\n'.join('extern const unsigned char '+n+'_spans[];\nextern const unsigned '+n+'_offsets[51];' for n,*_ in views)+'\n')
|
||||
(ROOT/'src/states.c').write_text('#include "quiz.h"\nconst State states[50] = {\n'+',\n'.join('{"%s","%s","%s"}'%tuple(r) for r in rows)+'\n};\n')
|
||||
meta['source_component_pixels']=counts
|
||||
(ROOT/'data/map-manifest.json').write_text(json.dumps(meta,indent=2)+'\n')
|
||||
print({n:meta[n]['bytes'] for n,*_ in views})
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal JSON-RPC client for the installed VCC Plus public STDIO MCP helper.
|
||||
No grant contents, private sockets, memory mutation or emulator internals used.
|
||||
"""
|
||||
import json,subprocess,sys
|
||||
from pathlib import Path
|
||||
HELPER='/Applications/VCC Plus.app/Contents/Helpers/vcc-mcp'
|
||||
class VCC:
|
||||
def __init__(self,grant):
|
||||
self.p=subprocess.Popen([HELPER,'--grant',str(grant)],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,bufsize=1)
|
||||
self.i=0
|
||||
self.request('initialize',{'protocolVersion':'2024-11-05','capabilities':{},'clientInfo':{'name':'capital-quiz-tests','version':'1.0'}})
|
||||
self.p.stdin.write(json.dumps({'jsonrpc':'2.0','method':'notifications/initialized'})+'\n');self.p.stdin.flush()
|
||||
def request(self,method,params):
|
||||
self.i+=1
|
||||
self.p.stdin.write(json.dumps({'jsonrpc':'2.0','id':self.i,'method':method,'params':params})+'\n');self.p.stdin.flush()
|
||||
while True:
|
||||
line=self.p.stdout.readline()
|
||||
if not line:raise RuntimeError(self.p.stderr.read())
|
||||
reply=json.loads(line)
|
||||
if reply.get('id')==self.i:
|
||||
if 'error' in reply:raise RuntimeError(reply['error'])
|
||||
return reply['result']
|
||||
def call(self,name,args=None):
|
||||
r=self.request('tools/call',{'name':name,'arguments':args or {}})
|
||||
for b in r.get('content',[]):
|
||||
if b.get('type')=='text':
|
||||
try:return json.loads(b['text'])
|
||||
except ValueError:pass
|
||||
return r
|
||||
def close(self):
|
||||
self.p.stdin.close();self.p.terminate();self.p.wait()
|
||||
if __name__=='__main__':
|
||||
v=VCC(sys.argv[1])
|
||||
try:
|
||||
if len(sys.argv)<3:result=v.request('tools/list',{})
|
||||
else:result=v.call(sys.argv[2],json.loads(sys.argv[3]) if len(sys.argv)>3 else {})
|
||||
print(json.dumps(result,indent=2))
|
||||
finally:v.close()
|
||||
Reference in new issue
Block a user