From 85b1ead5b8d76e7ca7975a35a33db7bb6be1ceec Mon Sep 17 00:00:00 2001 From: viking <163542798+Dev-v1@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:51:45 -0500 Subject: [PATCH] Keep desktop code and tracks in beebright-local; link to its releases --- .github/workflows/local-parity.yml | 42 ----- .../frontend/src/arcade-core.js | 2 +- .../frontend/src/game-mechanics.js | 8 - .../frontend/src/game-mechanics.test.js | 6 +- .../frontend/src/games-rally.js | 20 +-- .../frontend/src/racing-mechanics.test.js | 20 +-- .../frontend/src/rally-babylon.js | 2 +- .../frontend/src/rally-scenery.js | 75 -------- .../frontend/src/rally-scenery.test.js | 39 ---- .../frontend/src/rally-terrain.js | 3 +- README.md | 3 + local/CHANGELOG.md | 15 +- local/README.md | 169 +----------------- local/release.json | 2 +- tools/build-local.mjs | 133 +++----------- tools/test_local_ui.py | 12 +- 16 files changed, 50 insertions(+), 501 deletions(-) delete mode 100644 .github/workflows/local-parity.yml delete mode 100644 BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.js delete mode 100644 BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.test.js diff --git a/.github/workflows/local-parity.yml b/.github/workflows/local-parity.yml deleted file mode 100644 index b34c231..0000000 --- a/.github/workflows/local-parity.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Desktop parity -on: - pull_request: - push: - branches: [main] -permissions: - contents: read -jobs: - shared-practice: - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - run: npm ci - working-directory: BeeBright-Full-Stack/beebright-spelling-bee/frontend - - run: node tools/build-local.mjs --materialize - - run: node --test BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/*.test.js - - run: python3 -m unittest discover -s local -v - - run: sh -n local/install.sh - - run: sh tools/test-posix-runtime.sh - windows-desktop: - runs-on: windows-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - run: npm ci - working-directory: BeeBright-Full-Stack/beebright-spelling-bee/frontend - - run: node tools/build-local.mjs --materialize - - run: python -m unittest discover -s local -v - - run: ./tools/test-local-install.ps1 - shell: powershell - - run: ./tools/test-local-update.ps1 - shell: powershell - - name: Save actual racing views - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: BeeBright-racing-views - path: ${{ runner.temp }}/bee-racing-views/*.png - if-no-files-found: warn - retention-days: 7 diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js index 45f94f5..a1fa314 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js @@ -3,7 +3,7 @@ export const GAMES = [ {id:'sheep',title:'Sheep Escape',tag:'BRIDGE BUILDER',description:'Hold to grow a bridge upward, then release to lower it and lead your sheep across.',controls:'Hold mouse / touch / Space: grow · Release: lower bridge',color:'#a1d9a1'}, {id:'gravity',title:'Gravity Flip',tag:'ENDLESS RUNNER',description:'Swap floor and ceiling to dodge the neon barriers.',controls:'Space / tap: flip gravity',color:'#d5a2ff'}, {id:'bowling',title:'Pocket Bowling',tag:'FIVE FRAMES',description:'Aim carefully and knock down all ten pins. Two rolls per frame.',controls:'Left / right: aim · Up / down: power · Q / E: spin · Space / Roll: bowl',color:'#7ed9ee'}, - {id:'rally',title:'Neon Rally',tag:'3D · TWELVE TRACKS',description:'Race four fast GT opponents on longer circuits. Climb mountain switchbacks, brake for hairpins and pass on the straights.',controls:'Up: accelerate · Down: brake / reverse · Left / right: steer · R: cockpit / chase camera',color:'#ff97b3',three:true}, + {id:'rally',title:'Neon Rally',tag:'3D · FOUR TRACKS',description:'Race four fast GT opponents on longer circuits. Climb mountain switchbacks, brake for hairpins and pass on the straights.',controls:'Up: accelerate · Down: brake / reverse · Left / right: steer · R: cockpit / chase camera',color:'#ff97b3',three:true}, {id:'dash',title:'Neon Dash',tag:'FIVE LEVELS',description:'Five long portal courses: jump as a cube, fly a ship, flip a ball and steer a wave.',controls:'Hold / tap: jump, fly or flip · Blue platforms are safe',color:'#a8b6ff',local:true}, {id:'marble',title:'Marble Run 3D',tag:'3D · THREE COURSES',description:'Race against the clock through boosts, jump gaps, narrow turns and moving sweepers.',controls:'Arrow keys / WASD: roll · Space: jump · Q: brake',color:'#9de6da',local:true,three:true}, {id:'space',title:'Space Survival 3D',tag:'3D · ARCADE',description:'Battle formations of alien ships, dodge diving fighters and collect shield energy.',controls:'Arrow keys / WASD: fly · Space: fire',color:'#e7bbff',local:true,three:true}, diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js index 18c2775..06a8981 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js @@ -123,14 +123,6 @@ export const RALLY_COURSES=[ {name:'Canyon Switchback',width:18,scale:2.8,heights:[0,8,18,12,4,0,7,20,13,5,2,10,5],laps:2,theme:'canyon',points:[[30,0],[32,24],[9,29],[4,10],[-13,9],[-14,32],[-36,23],[-34,-20],[-14,-30],[-2,-10],[13,-15],[17,-33],[34,-25]]}, {name:'Alpine Hairpins',width:18,scale:1,heights:[0,8,20,36,46,60,65,58,53,47,40,36,28,18,8,4,2,0,0],laps:2,theme:'alpine',mountains:[[-35,155,110,90],[155,115,145,80],[-170,95,135,85],[0,-180,90,80]],points:[[145,-100],[170,-35],[140,35],[80,60],[70,120],[0,150],[-65,110],[-35,70],[25,80],[30,40],[-40,25],[-115,55],[-150,0],[-135,-65],[-75,-90],[-45,-40],[15,-55],[55,-100],[80,-145]]}, {name:'Midnight Metro',width:20,scale:2.8,heights:[0,2,6,4,0,0,10,15,8,2,0,0,3,0,5],laps:2,theme:'city',points:[[35,0],[34,28],[10,29],[8,12],[-10,12],[-12,32],[-34,28],[-36,4],[-20,2],[-20,-18],[-35,-20],[-25,-36],[3,-34],[7,-17],[32,-24]]}, - {name:'Coastal Sprint',scenery:'coast',width:20,scale:1,heights:[1,3,6,8,7,4,1,0,0,1],laps:2,theme:'harbor',points:[[150,0],[145,65],[80,100],[-35,105],[-145,65],[-170,-10],[-120,-90],[-20,-105],[90,-90],[155,-55]]}, - {name:'Red Mesa Run',scenery:'mesa',width:18,scale:1,heights:[0,5,14,25,31,27,20,12,6,2,0,0],laps:2,theme:'canyon',points:[[160,0],[150,75],[75,120],[0,95],[-70,130],[-155,85],[-175,0],[-130,-75],[-55,-110],[10,-75],[80,-110],[150,-65]]}, - {name:'Pine Ridge',scenery:'ridge',width:18,scale:1,heights:[0,6,14,22,30,35,30,24,17,10,4,0],laps:2,theme:'alpine',mountains:[[-220,160,65,90],[190,205,78,95],[-205,-155,60,85],[50,-220,68,90]],points:[[140,-10],[130,65],[60,105],[-10,70],[-75,115],[-145,75],[-165,0],[-120,-80],[-40,-110],[25,-60],[95,-100],[150,-65]]}, - {name:'Old Town Loop',scenery:'town',width:18,scale:1,heights:[0,1,4,8,7,4,1,0,0,2,5,6,3,0],laps:2,theme:'city',points:[[150,0],[145,85],[65,110],[5,65],[-55,110],[-145,85],[-165,10],[-120,-25],[-145,-105],[-55,-120],[5,-70],[75,-115],[155,-95],[185,-40]]}, - {name:'Summit Grand Prix',scenery:'summit',width:18,scale:1,heights:[0,8,20,35,50,63,70,62,52,42,32,22,12,4,0,0],laps:2,theme:'alpine',mountains:[[-240,160,140,100],[205,210,155,95],[-210,-180,125,100],[40,-245,130,90]],points:[[185,-60],[190,30],[145,105],[70,150],[-15,115],[-85,165],[-170,120],[-200,40],[-150,-20],[-200,-95],[-155,-170],[-65,-190],[0,-125],[70,-175],[150,-150],[205,-115]]}, - {name:'Sunset Docks',scenery:'docks',width:20,scale:1,heights:[0,2,5,9,12,10,5,1,0,2,6,9,5,0],laps:2,theme:'harbor',points:[[160,-10],[150,70],[80,120],[0,90],[-80,125],[-160,70],[-175,-10],[-105,-50],[-155,-125],[-65,-150],[10,-105],[85,-150],[165,-110],[200,-65]]}, - {name:'Aurora Fjord',scenery:'aurora',width:18,scale:1,heights:[0,4,11,20,28,34,30,23,15,8,3,0],laps:2,theme:'aurora',mountains:[[-210,170,90,85],[200,170,110,95],[-220,-150,80,90]],points:[[150,-15],[150,65],[80,120],[0,85],[-70,130],[-150,85],[-180,0],[-125,-75],[-50,-120],[25,-75],[100,-115],[165,-75]]}, - {name:'Polar Night Pass',scenery:'polar',width:18,scale:1,heights:[0,6,15,26,39,44,38,30,21,12,5,0,0,0],laps:2,theme:'polar',mountains:[[-215,175,110,95],[210,190,125,85],[-220,-180,95,90],[40,-220,80,80]],points:[[165,-35],[175,50],[110,120],[30,155],[-40,110],[-120,155],[-185,85],[-195,-5],[-125,-45],[-165,-125],[-75,-165],[5,-110],[95,-165],[180,-110]]}, ]; export function roadDistance(points,x,z){let best=Infinity;for(let i=0;i=next.x&&tip<=next.x+next.w;} diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.test.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.test.js index 3441064..7802167 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.test.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.test.js @@ -64,10 +64,8 @@ test('every full Dash course is completable across all four modes without losing } }); test('Rally tracks differ and road proximity follows their curves, not an ellipse',()=>{ - assert.equal(RALLY_COURSES.length,12);assert.equal(new Set(RALLY_COURSES.map(c=>c.theme)).size,6); - assert.equal(new Set(RALLY_COURSES.map(c=>c.name)).size,RALLY_COURSES.length); - assert.equal(new Set(RALLY_COURSES.map(c=>JSON.stringify(c.points))).size,RALLY_COURSES.length); - for(const c of RALLY_COURSES){const curve=new THREE.CatmullRomCurve3(c.points.map(([x,z])=>new THREE.Vector3(x,0,z)),true,'centripetal'),p=curve.getSpacedPoints(128).slice(0,-1);assert.ok(roadDistance(p,p[20].x,p[20].z)<.01);assert.ok(roadDistance(p,10000,10000)>c.width);assert.ok(curve.getLength()>150);} + assert.equal(RALLY_COURSES.length,4);assert.equal(new Set(RALLY_COURSES.map(c=>c.theme)).size,4); + for(const c of RALLY_COURSES){const curve=new THREE.CatmullRomCurve3(c.points.map(([x,z])=>new THREE.Vector3(x,0,z)),true,'centripetal'),p=curve.getSpacedPoints(128).slice(0,-1);assert.ok(roadDistance(p,p[20].x,p[20].z)<.01);assert.ok(roadDistance(p,100,100)>c.width);assert.ok(curve.getLength()>150);} for(let i=0;i<3;i++){const path=marblePath(i);assert.ok(path.length>=11);assert.ok(path.filter(s=>s.gap).length>=2);} }); test('banked space ship corners stay visible under sustained movement toward every edge',()=>{ diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js index bc71bb4..8299fb5 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js @@ -5,7 +5,6 @@ import {rallyRoad,rallySample,newRallyRace,rallyStep,newRallyView,rallyViewStep} import {buildRallyCar,rallyNavigation} from './rally-car.js'; export {buildRallyCar} from './rally-car.js'; import {rallyTerrain} from './rally-terrain.js'; -import {rallyEnvironment,addRallyScenery} from './rally-scenery.js'; // Continuous ribbons share section vertices; inclines have no stacked road boxes. export function rallyRibbon(road,inner,outer,lift=0){ const pos=[],uv=[],indices=[],n=road.points.length; @@ -16,42 +15,40 @@ export function rallyRibbon(road,inner,outer,lift=0){ const geo=new THREE.BufferGeometry();geo.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));geo.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));geo.setIndex(indices);geo.computeVertexNormals();return geo; } export function setupRally({scene,camera,light,high,level,laps,player,mesh,mat,textures,materials,finish,notify}){ - const course=RALLY_COURSES[level]||RALLY_COURSES[0],road=rallyRoad(course,laps),race=newRallyRace(road),view=newRallyView(),style=rallyEnvironment(course),night=style?.night??(course.theme==='city'); - scene.background=new THREE.Color(style?.sky||(night?'#14243a':course.theme==='canyon'?'#cda385':'#a7c9dd'));scene.fog=new THREE.Fog(scene.background,180,540);camera.far=650;camera.updateProjectionMatrix(); + const course=RALLY_COURSES[level]||RALLY_COURSES[0],road=rallyRoad(course,laps),race=newRallyRace(road),view=newRallyView(),night=course.theme==='city'; + scene.background=new THREE.Color(night?'#14243a':course.theme==='canyon'?'#cda385':'#a7c9dd');scene.fog=new THREE.Fog(scene.background,180,540);camera.far=650;camera.updateProjectionMatrix(); light.color.set(night?'#a7cfff':'#ffe5c5');light.intensity=night?2.1:3.4; light.shadow.bias=-.00012;light.shadow.normalBias=.025; Object.assign(light.shadow.camera,{left:-32,right:32,top:32,bottom:-32,far:140}); if(high){const sky=new Uint8Array(256*128*4);for(let y=0;y<128;y++)for(let x=0;x<256;x++){const h=y/127,sun=Math.exp(-((x-172)**2+(y-40)**2)/35),i=(y*256+x)*4;sky[i]=night?18+35*h:80+150*h+sun*25;sky[i+1]=night?28+50*h:145+100*h;sky[i+2]=night?50+65*h:200+35*h;sky[i+3]=255;} - if(style){const top=new THREE.Color(style.sky).convertLinearToSRGB(),horizon=new THREE.Color(style.horizon).convertLinearToSRGB();for(let y=0;y<128;y++){const color=top.clone().lerp(horizon,y/127);for(let x=0;x<256;x++){const i=(y*256+x)*4;sky[i]=color.r*255;sky[i+1]=color.g*255;sky[i+2]=color.b*255;}}} const env=new THREE.DataTexture(sky,256,128);env.mapping=THREE.EquirectangularReflectionMapping;env.colorSpace=THREE.SRGBColorSpace;env.needsUpdate=true;scene.environment=env;scene.background=env;textures.push(env); } - const asphalt=mat(style?.road||'#3b4147');asphalt.roughness=.92;asphalt.metalness=0; + const asphalt=mat('#3b4147');asphalt.roughness=.92;asphalt.metalness=0; if(high){const data=new Uint8Array(64*64*4);for(let i=0;i<4096;i++){const v=145+(i*31+i%19*7)%65;data[i*4]=data[i*4+1]=data[i*4+2]=v;data[i*4+3]=255;}const grain=new THREE.DataTexture(data,64,64);grain.wrapS=grain.wrapT=THREE.RepeatWrapping;grain.needsUpdate=true;asphalt.map=grain;asphalt.bumpMap=grain;asphalt.bumpScale=.025;textures.push(grain);} mesh(rallyRibbon(road,-course.width/2,course.width/2),asphalt); - const verge=mat(style?.verge||(course.theme==='canyon'?'#a87856':course.theme==='alpine'?'#739371':'#7c947c'));verge.metalness=0;verge.roughness=1; + const verge=mat(course.theme==='canyon'?'#a87856':course.theme==='alpine'?'#739371':'#7c947c');verge.metalness=0;verge.roughness=1; for(const side of [-1,1]){const lo=side<0?-course.width/2-3:course.width/2,hi=lo+3;mesh(rallyRibbon(road,lo,hi,-.08),verge);} - const red=mat(style?.accent||'#bb4b43'),white=mat('#e6dfce');red.metalness=white.metalness=0; + const red=mat('#bb4b43'),white=mat('#e6dfce');red.metalness=white.metalness=0; for(const side of [-1,1]){const lo=side<0?-course.width/2-.5:course.width/2;mesh(rallyRibbon(road,lo,lo+.5,.045),red);} const stripe=mat('#fff5cc');stripe.polygonOffset=true;stripe.polygonOffsetFactor=-1;stripe.polygonOffsetUnits=-1;mesh(rallyRibbon(road,-.05,.05,.014),stripe); // Elevated roads are supported by piers; terrain does not clip through the road. - const terrainMat=mat(style?.terrain?'#ffffff':style?.ground||(course.theme==='alpine'?'#ffffff':course.theme==='canyon'?'#b18a65':'#719077'));terrainMat.vertexColors=Boolean(style?.terrain)||course.theme==='alpine';terrainMat.metalness=0;terrainMat.roughness=.96;const terrain=mesh(rallyTerrain(course,road,high),terrainMat);terrain.userData.softwareHidden=true; + const terrainMat=mat(course.theme==='alpine'?'#ffffff':course.theme==='canyon'?'#b18a65':'#719077');terrainMat.vertexColors=course.theme==='alpine';terrainMat.metalness=0;terrainMat.roughness=.96;const terrain=mesh(rallyTerrain(course,road,high),terrainMat);terrain.userData.softwareHidden=true; for(let i=0;i1)mesh(new THREE.CylinderGeometry(.75,1.1,point.y+1.1,8),mat('#949996'),point.x,(point.y-1.1)/2,point.z);} const start=rallySample(road,0),banner=new THREE.Group();scene.add(banner);banner.position.set(start.x,start.y,start.z);banner.rotation.y=-start.heading; mesh(new THREE.BoxGeometry(.15,5,.15),white,0,2.5,-course.width/2-1,banner);mesh(new THREE.BoxGeometry(.15,5,.15),white,0,2.5,course.width/2+1,banner);mesh(new THREE.BoxGeometry(.25,.6,course.width+2.2),red,0,5,0,banner); for(let i=0;i<14;i++)mesh(new THREE.BoxGeometry(.8,.016,course.width/14),i%2?white:asphalt,0,.018,-course.width/2+(i+.5)*course.width/14,banner); // Trackside details are distributed along the longer routes, away from the road. const treeMat=mat('#345342'),bark=mat('#62574d'),rockMat=mat('#93968a'),buildingMat=mat(night?'#354352':'#7c898b'); - const count=style?0:high?90:28; + const count=high?90:28; for(let i=0;i20){const side=bend>0?-1:1,g=rallySample(road,d,side*(course.width/2+1));const beam=mesh(new THREE.BoxGeometry(high?12:28,.28,.12),rail,g.x,g.y+.8,g.z);beam.rotation.y=-g.heading;beam.rotation.z=Math.atan(g.grade);mesh(new THREE.CylinderGeometry(.055,.055,1.15,6),rail,g.x,g.y+.42,g.z);} + if(course.theme==='alpine'&&f.y>20){const side=bend>0?-1:1,g=rallySample(road,d,side*(course.width/2+1));const beam=mesh(new THREE.BoxGeometry(high?12:28,.28,.12),rail,g.x,g.y+.8,g.z);beam.rotation.y=-g.heading;beam.rotation.z=Math.atan(g.grade);mesh(new THREE.CylinderGeometry(.055,.055,1.15,6),rail,g.x,g.y+.42,g.z);} if(Math.abs(bend)>.65&&Math.floor(d/12)%3===0){const g=rallySample(road,d,course.width/2+2),sign=mesh(new THREE.BoxGeometry(.08,.9,1.1),white,g.x,g.y+1.8,g.z);sign.rotation.y=-g.heading;mesh(new THREE.CylinderGeometry(.055,.055,1.5,6),rail,g.x,g.y+.75,g.z);} } const paints=['#d84e71','#3da5bd','#f3bd47','#536ed0','#78a755'],cars=[]; @@ -67,7 +64,6 @@ export function setupRally({scene,camera,light,high,level,laps,player,mesh,mat,t if(view.changed){camera.fov=view.cockpit?88:58;camera.near=view.cockpit?.025:.1;camera.updateProjectionMatrix();} if(view.cockpit){player.updateMatrixWorld(true);const eye=player.localToWorld(new THREE.Vector3(-.64,1.23,.34)),target=player.localToWorld(new THREE.Vector3(24,1.18,.34));camera.position.copy(eye);camera.up.set(0,1,0);camera.lookAt(target);} else{const behind=new THREE.Vector3(p.x-Math.cos(p.heading)*10,p.y+4.2,p.z-Math.sin(p.heading)*10);if(view.changed)camera.position.copy(behind);else camera.position.lerp(behind,1-Math.exp(-dt*7));const look=rallySample(road,p.travel+8);camera.lookAt(p.x+Math.cos(p.heading)*6,(p.y+look.y)/2+.85,p.z+Math.sin(p.heading)*6);} - animateScenery(race.time,camera); } const p=race.player;camera.position.set(p.x-Math.cos(p.heading)*10,p.y+5.3,p.z-Math.sin(p.heading)*10);render(0); return {player:p,road,race,view,pause(input){view.previous=Boolean(input.camera);},update(input,dt){rallyViewStep(view,input.camera);race.input=input;rallyStep(race,road,input,dt);render(dt); diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js index 2019337..a56f4d0 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js @@ -32,7 +32,7 @@ test('rally progress unwraps forward and reverse at the line and rejects telepor test('four opponents drive every circuit uphill and through corners without leaving its road',()=>{ for(const course of RALLY_COURSES){const road=rallyRoad(course),race=newRallyRace(road);assert.equal(race.opponents.length,4); for(let i=0;i<20000&&!race.opponents.every(c=>c.finished);i++)rallyStep(race,road,{},1/60); - assert.ok(race.opponents.every(c=>c.finished));assert.ok(race.opponents.every(c=>c.finishTime>=road.length*course.laps/(c.cruise*1.12)-1/60),'finishing time respects each rival\'s maximum cruise speed');assert.equal(race.finished,false,'AI finishing does not stop the player'); + assert.ok(race.opponents.every(c=>c.finished));assert.ok(race.opponents.every(c=>c.finishTime>road.length*course.laps/40));assert.equal(race.finished,false,'AI finishing does not stop the player'); for(const car of race.opponents){const hit=rallyProject(road,car.x,car.z);assert.ok(hit.distance{ +test('all four races can be finished through every checkpoint with ordinary driving inputs',()=>{ for(const course of RALLY_COURSES){const road=rallyRoad(course),race=newRallyRace(road);let frames=0; while(!race.finished&&frames++<20000){const p=race.player,look=rallySample(road,p.travel+Math.max(8,p.speed*.4)),heading=Math.atan2(look.z-p.z,look.x-p.x),error=Math.atan2(Math.sin(heading-p.heading),Math.cos(heading-p.heading)),target=rallyCornerPlan(road,p.travel,38).speed; rallyStep(race,road,{up:p.speedtarget+1,right:error>.025,left:error<-.025},1/60); @@ -93,22 +93,6 @@ test('grades blend continuously through every segment and the closed start-line for(let d=0;d{ - for(const course of RALLY_COURSES.slice(4)){ - const road=rallyRoad(course),terrain=rallyTerrain(course,road,false);terrain.computeBoundingBox(); - const box=terrain.boundingBox; - for(let i=0;ibox.min.x&&p.x+marginbox.min.z&&p.z+margincourse.width*3)assert.ok(Math.hypot(p.x-road.points[j].x,p.z-road.points[j].z)>course.width+8,course.name+' has no overlapping road sections'); - } - } - terrain.dispose(); - } -}); test('rivals accelerate past 40 metres per second while retaining corner braking',()=>{ const road=rallyRoad(RALLY_COURSES[0]),race=newRallyRace(road);let fastest=0; assert.ok(race.opponents.every(c=>c.cruise>=52)); diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js index c39fc96..7b6453e 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js @@ -46,7 +46,7 @@ export async function createRallyRenderer(canvas,onGraphics){ else if(image?.getContext){result=new DynamicTexture('instrument-screen',image,scene,true);dynamic.push(result);result.update(true);} if(result){result.wrapU=source.wrapS===1000?Texture.WRAP_ADDRESSMODE:Texture.CLAMP_ADDRESSMODE;result.wrapV=source.wrapT===1000?Texture.WRAP_ADDRESSMODE:Texture.CLAMP_ADDRESSMODE;result.uScale=source.repeat?.x??1;result.vScale=source.repeat?.y??1;textures.set(source,result);}return result; } - function material(source){if(materials.has(source))return materials.get(source);const m=new PBRMaterial('gt-material',scene);m.albedoColor=color(source.color);m.metallic=source.metalness??0;m.roughness=source.roughness??.7;m.emissiveColor=color(source.emissive).scale(source.emissiveIntensity??0);m.alpha=source.opacity??1;m.fogEnabled=source.fog!==false;m.disableDepthWrite=source.depthWrite===false;m.backFaceCulling=source.side!==2;m.sideOrientation=Material.CounterClockWiseSideOrientation;m.useVertexColors=Boolean(source.vertexColors);m.albedoTexture=texture(source.map);m.emissiveTexture=texture(source.emissiveMap);m.bumpTexture=texture(source.bumpMap);if(m.bumpTexture)m.bumpTexture.level=source.bumpScale??.02; + function material(source){if(materials.has(source))return materials.get(source);const m=new PBRMaterial('gt-material',scene);m.albedoColor=color(source.color);m.metallic=source.metalness??0;m.roughness=source.roughness??.7;m.emissiveColor=color(source.emissive).scale(source.emissiveIntensity??0);m.alpha=source.opacity??1;m.backFaceCulling=source.side!==2;m.sideOrientation=Material.CounterClockWiseSideOrientation;m.useVertexColors=Boolean(source.vertexColors);m.albedoTexture=texture(source.map);m.emissiveTexture=texture(source.emissiveMap);m.bumpTexture=texture(source.bumpMap);if(m.bumpTexture)m.bumpTexture.level=source.bumpScale??.02; if(source.clearcoat){m.clearCoat.isEnabled=true;m.clearCoat.intensity=source.clearcoat;m.clearCoat.roughness=source.clearcoatRoughness??.12;}if(source.transparent){m.transparencyMode=PBRMaterial.PBRMATERIAL_ALPHABLEND;m.separateCullingPass=true;}materials.set(source,m);return m; } function convert(source,parent=null){let target; diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.js deleted file mode 100644 index 45ebcaa..0000000 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.js +++ /dev/null @@ -1,75 +0,0 @@ -import * as THREE from 'three'; - -// Bundled colors and original procedural scenery: no image downloads or network access. -export const RALLY_ENVIRONMENTS={ - coast:{sky:'#559cbd',horizon:'#ffe4b2',ground:'#dfc28b',verge:'#e4ce9f',road:'#4b5159',accent:'#36c5b6',props:'palms'}, - mesa:{sky:'#c58262',horizon:'#ffd19b',ground:'#a95537',verge:'#d38c57',road:'#493d37',accent:'#ffd06d',props:'cacti'}, - ridge:{sky:'#7a9ca2',horizon:'#edd1a5',ground:'#717e42',verge:'#a39355',road:'#3b4340',accent:'#f4ab4c',props:'autumn',terrain:['#718044','#80765a','#eee0bf']}, - town:{sky:'#72649b',horizon:'#f7b7a2',ground:'#987c79',verge:'#c9b4a2',road:'#574d62',accent:'#ef96c4',props:'houses'}, - summit:{sky:'#42759c',horizon:'#d9f1ff',ground:'#e1edf0',verge:'#b9d5df',road:'#445466',accent:'#61d7f0',props:'snow',terrain:['#93b8c5','#c0d2dc','#f3fbff']}, - docks:{sky:'#874b64',horizon:'#ffb274',ground:'#325f73',verge:'#9c826d',road:'#4a4249',accent:'#ffb869',props:'cargo'}, - aurora:{sky:'#071c32',horizon:'#173b4f',ground:'#adcdd9',verge:'#d4edf0',road:'#364c60',accent:'#63ffd3',props:'frozen',night:true,aurora:true,terrain:['#87b2c2','#bdd9e1','#ecffff']}, - polar:{sky:'#151530',horizon:'#30345c',ground:'#c1bbdf',verge:'#e5def7',road:'#464960',accent:'#ce9cff',props:'crystals',night:true,aurora:true,terrain:['#909ac0','#c5c4df','#f0ecff']}, -}; -export const rallyEnvironment=course=>RALLY_ENVIRONMENTS[course.scenery]||null; - -export function auroraCurtain(radius,phase,segments=80){ - const positions=[],colors=[],indices=[]; - for(let i=0;i<=segments;i++){ - const a=i/segments*Math.PI*2,x=Math.cos(a)*radius,z=Math.sin(a)*radius; - const base=50+12*Math.sin(a*3+phase)+7*Math.cos(a*7-phase); - const tint=new THREE.Color().setHSL(.39+.2*(.5+.5*Math.sin(a*2+phase)),.9,.58); - for(const height of [base,base+28+12*Math.sin(a*5+phase)]){ - positions.push(x,height,z);colors.push(tint.r,tint.g,tint.b); - } - if(i{}; - const leaf=mat(style.props==='autumn'?'#d88336':'#35866d'),trunk=mat('#79604e'),stone=mat(style.ground),ice=mat('#a2e4f0'),wood=mat('#997258'); - const count=high?54:26; - for(let i=0;i{sky.position.set(camera.position.x,0,camera.position.z);curtains.forEach((curtain,i)=>{curtain.rotation.y=Math.sin(time*.06+i)*.035;curtain.material.emissiveIntensity=.8+.18*Math.sin(time*.3+i);});}; - } - return ()=>{}; -} diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.test.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.test.js deleted file mode 100644 index bf88d8b..0000000 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import * as THREE from 'three'; -import {RALLY_COURSES} from './game-mechanics.js'; -import {rallyRoad,rallySample} from './racing-mechanics.js'; -import {rallyEnvironment,addRallyScenery,auroraCurtain} from './rally-scenery.js'; - -test('original tracks retain their scenery while all eight additions have different environments',()=>{ - assert.deepEqual(RALLY_COURSES.slice(0,4).map(c=>[c.name,c.width,c.scenery]),[['Harbor Circuit',20,undefined],['Canyon Switchback',18,undefined],['Alpine Hairpins',18,undefined],['Midnight Metro',20,undefined]]); - const added=RALLY_COURSES.slice(4); - assert.equal(added.length,8);assert.equal(new Set(added.map(c=>c.scenery)).size,8); - assert.equal(new Set(added.map(c=>rallyEnvironment(c).sky)).size,8); - for(const course of added){assert.ok(course.width>=18&&course.width<=20);assert.ok(rallyEnvironment(course).props);} - assert.equal(added.filter(c=>rallyEnvironment(c).aurora).length,2); -}); - -test('aurora curtains form closed finite ribbons with visible color variation',()=>{ - for(const segments of [48,112]){ - const geometry=auroraCurtain(320,1.7,segments),positions=geometry.attributes.position,colors=geometry.attributes.color; - assert.equal(positions.count,(segments+1)*2);assert.equal(geometry.index.count,segments*6); - assert.ok(new Set(Array.from(colors.array).map(v=>v.toFixed(3))).size>20); - for(const value of positions.array)assert.ok(Number.isFinite(value)); - for(const value of colors.array)assert.ok(value>=0&&value<=1); - for(let j=0;j<2;j++)assert.ok(new THREE.Vector3().fromBufferAttribute(positions,j).distanceTo(new THREE.Vector3().fromBufferAttribute(positions,segments*2+j))<.001); - geometry.dispose(); - } -}); - -test('all new scenery builds in both qualities and northern lights animate without casting shadows',()=>{ - for(const high of [false,true])for(const course of RALLY_COURSES.slice(4)){ - const scene=new THREE.Scene(),geometries=[],materials=[],road=rallyRoad(course); - const mat=color=>{const m=new THREE.MeshStandardMaterial({color});materials.push(m);return m;}; - const mesh=(g,m,x=0,y=0,z=0,parent=scene)=>{geometries.push(g);const node=new THREE.Mesh(g,m);node.position.set(x,y,z);parent.add(node);return node;}; - const animate=addRallyScenery({course,road,high,scene,mesh,mat,sample:rallySample}),camera=new THREE.PerspectiveCamera();camera.position.set(30,4,-20);animate(2,camera); - assert.ok(geometries.length>20,course.name); - if(rallyEnvironment(course).aurora){const curtain=scene.getObjectByName('northern-lights-0'),before=curtain.rotation.y;assert.ok(curtain);assert.equal(curtain.castShadow,false);assert.equal(curtain.material.fog,false);assert.equal(curtain.parent.position.x,30);animate(8,camera);assert.notEqual(curtain.rotation.y,before);} - for(const geometry of geometries)geometry.dispose();for(const material of materials)material.dispose(); - } -}); diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js index b95c625..35ed874 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js @@ -1,8 +1,7 @@ import * as THREE from 'three'; import {clamp} from './game-mechanics.js'; -import {rallyEnvironment} from './rally-scenery.js'; export function mountainHeight(course,x,z){let h=-2;for(const [mx,mz,peak,radius] of course.mountains||[])h+=peak*Math.exp(-((x-mx)**2+(z-mz)**2)/(radius*radius));return h+Math.sin(x*.07)*Math.cos(z*.055)*3;} -export function rallyTerrain(course,road,high){const alpine=course.theme==='alpine'||Boolean(course.mountains),style=rallyEnvironment(course),extent=Math.max(...road.points.flatMap(p=>[Math.abs(p.x),Math.abs(p.z)])),size=Math.max(alpine?650:360,2*(extent+course.width/2+50)),geo=new THREE.PlaneGeometry(size,size,alpine?(high?128:64):1,alpine?(high?128:64):1);geo.rotateX(-Math.PI/2);const pos=geo.attributes.position,colors=[],grass=new THREE.Color(style?.terrain?.[0]||'#657755'),rock=new THREE.Color(style?.terrain?.[1]||'#858885'),snow=new THREE.Color(style?.terrain?.[2]||'#e4e9e8'); +export function rallyTerrain(course,road,high){const alpine=course.theme==='alpine',size=alpine?650:360,geo=new THREE.PlaneGeometry(size,size,alpine?(high?128:64):1,alpine?(high?128:64):1);geo.rotateX(-Math.PI/2);const pos=geo.attributes.position,colors=[],grass=new THREE.Color('#657755'),rock=new THREE.Color('#858885'),snow=new THREE.Color('#e4e9e8'); for(let k=0;k JSON.parse(fs.readFileSync(path.join(backend, 'data', name), 'utf8')); -const hints = {...readJson('word_hints.json'), ...readJson('study_2027_hints.json')}; -const lists = [{id:'champions-2024',title:'2024 Words of the Champions',...readJson('words.json')}, - readJson('word_lists/study-2027.json')]; -const frontend = path.join(app,'frontend'); -fs.writeFileSync(path.join(frontend,'public/study-catalog.json'),JSON.stringify({lists,hints,distractors:readJson('distractors.json')})); -fs.copyFileSync(path.join(local,'CHANGELOG.md'),path.join(frontend,'public/local-changelog.md')); - -// Compile the exact website components/CSS with an offline-only desktop entry point. -execFileSync(process.execPath,[path.join(frontend,'node_modules/vite/bin/vite.js'),'build','--mode','desktop'],{cwd:frontend,stdio:'inherit'}); -const files = new Map(); -function walk(dir, prefix='') { - for (const item of fs.readdirSync(dir, {withFileTypes:true})) { - if (['__pycache__','data','shared','ui','ui-source','.git'].includes(item.name)) continue; - const name = path.posix.join(prefix,item.name); - if (item.isDirectory()) walk(path.join(dir,item.name), name); - else if (!item.name.endsWith('.pyc')) files.set(name,fs.readFileSync(path.join(dir,item.name))); - } -} -walk(local); -function collect(dir,prefix) { - for (const entry of fs.readdirSync(dir,{withFileTypes:true})) { - const file=path.join(dir,entry.name), name=path.posix.join(prefix,entry.name); - if (entry.isDirectory()) collect(file,name); else files.set(name,fs.readFileSync(file)); - } -} -collect(path.join(frontend,'dist-local'),'beebright_local/ui'); -files.set('beebright_local/ui/CHANGELOG.md',fs.readFileSync(path.join(local,'CHANGELOG.md'))); -files.set('beebright_local/ui/bee.svg',fs.readFileSync(path.join(frontend,'public/bee.svg'))); -for(const name of ['App.jsx','api.js','hints.js','origins.js','speech.js','local-api.js','local-main.jsx','styles.css','fonts.css','studio.jsx','studio-core.js','arcade-core.js','arcade.jsx','arcade.css','games-2d.js','games-3d.js','game-renderer.js','game-mechanics.js','track-geometry.js','games-rally.js','racing-mechanics.js','rally-car.js','rally-terrain.js','rally-scenery.js','rally-babylon.js']) { - let source=fs.readFileSync(path.join(frontend,'src',name)); - if(name==='App.jsx') source=Buffer.from(source.toString().replace('../../../../local/release.json','../../release.json')); - files.set('ui-source/src/'+name,source); -} -for(const name of ['package.json','package-lock.json','vite.config.js','local.html']) { - files.set('ui-source/'+name,fs.readFileSync(path.join(frontend,name))); -} -files.set('THIRD_PARTY_THREE_LICENSE.txt',fs.readFileSync(path.join(frontend,'node_modules/three/LICENSE'))); -files.set('THIRD_PARTY_BABYLON_LICENSE.txt',fs.readFileSync(path.join(frontend,'node_modules/@babylonjs/core/license.md'))); -files.set('LICENSE',fs.readFileSync(path.join(root,'LICENSE'))); -for (const [name, data] of Object.entries({hints,lists,distractors:readJson('distractors.json')})) { - files.set(`beebright_local/data/${name}.json`,Buffer.from(JSON.stringify(data))); -} -files.set('beebright_local/shared/__init__.py',Buffer.from('')); -for (const name of ['practice_core.py','distractors.py','origins.py']) { - files.set(`beebright_local/shared/${name}`,fs.readFileSync(path.join(backend,'app/services',name))); -} -let version = process.env.VERCEL_GIT_COMMIT_SHA; -if (!version) version = execFileSync('git',['rev-parse','HEAD'],{cwd:root,encoding:'utf8'}).trim(); -files.set('version.json',Buffer.from(JSON.stringify({version}))); - -// Standard ZIP, deterministic bytes, no external zip program or dependencies. -function crc32(data) { - let crc = 0xffffffff; - for (const byte of data) { - crc ^= byte; - for (let bit=0;bit<8;bit++) crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0); - } - return (crc ^ 0xffffffff) >>> 0; -} -let offset=0; -const bodies=[], directory=[]; -for (const [name, content] of [...files].sort(([a],[b])=>a.localeCompare(b))) { - const filename=Buffer.from(name), compressed=zlib.deflateRawSync(content), crc=crc32(content); - const header=Buffer.alloc(30); - header.writeUInt32LE(0x04034b50); header.writeUInt16LE(20,4); - header.writeUInt16LE(0x800,6); header.writeUInt16LE(8,8); - header.writeUInt16LE(0x21,12); header.writeUInt32LE(crc,14); - header.writeUInt32LE(compressed.length,18); header.writeUInt32LE(content.length,22); header.writeUInt16LE(filename.length,26); - bodies.push(header,filename,compressed); - const central=Buffer.alloc(46); - central.writeUInt32LE(0x02014b50); central.writeUInt16LE(20,4); central.writeUInt16LE(20,6); - central.writeUInt16LE(0x800,8); central.writeUInt16LE(8,10); central.writeUInt16LE(0x21,14); - central.writeUInt32LE(crc,16); central.writeUInt32LE(compressed.length,20); central.writeUInt32LE(content.length,24); - central.writeUInt16LE(filename.length,28); central.writeUInt32LE(offset,42); - directory.push(central,filename); offset += header.length+filename.length+compressed.length; -} -const central=Buffer.concat(directory), end=Buffer.alloc(22); -end.writeUInt32LE(0x06054b50); end.writeUInt16LE(files.size,8); end.writeUInt16LE(files.size,10); -end.writeUInt32LE(central.length,12); end.writeUInt32LE(offset,16); -const archive=Buffer.concat([...bodies,central,end]); -fs.mkdirSync(destination,{recursive:true}); -fs.writeFileSync(path.join(destination,'beebright-local.zip'),archive); -fs.writeFileSync(path.join(destination,'manifest.json'),JSON.stringify({version, - url:'https://beebright.vercel.app/local/beebright-local.zip', - sha256:crypto.createHash('sha256').update(archive).digest('hex')},null,2)); -fs.copyFileSync(path.join(local,'bootstrap.ps1'),path.join(destination,'bootstrap.ps1')); -fs.copyFileSync(path.join(local,'install.sh'),path.join(app,'frontend/public/install.sh')); -fs.copyFileSync(path.join(local,'bootstrap.py'),path.join(destination,'bootstrap.py')); -fs.copyFileSync(path.join(local,'install.ps1'),path.join(app,'frontend/public/install.ps1')); -// A local developer can run the desktop edition from this same generated package. -if (process.argv.includes('--materialize')) { - for (const [name,data] of files) { - const dest=path.join(local,name); fs.mkdirSync(path.dirname(dest),{recursive:true}); fs.writeFileSync(dest,data); - } -} -console.log(`Bundled BeeBright local: ${files.size} files, ${archive.length} bytes, ${version.slice(0,12)}`); - +import {fileURLToPath} from 'node:url'; +const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); +const output=path.join(root,'BeeBright-Full-Stack/beebright-spelling-bee/frontend/public'); +const backend=path.join(root,'BeeBright-Full-Stack/beebright-spelling-bee/backend/data'); +const readJson=async name=>JSON.parse(await fs.readFile(path.join(backend,name),'utf8')); +const hints={...await readJson('word_hints.json'),...await readJson('study_2027_hints.json')}; +const lists=[{id:'champions-2024',title:'2024 Words of the Champions',...await readJson('words.json')},await readJson('word_lists/study-2027.json')]; +await fs.writeFile(path.join(output,'study-catalog.json'),JSON.stringify({lists,hints,distractors:await readJson('distractors.json')})); +await fs.copyFile(path.join(root,'local/CHANGELOG.md'),path.join(output,'local-changelog.md')); +const repository='https://raw.githubusercontent.com/Dev-v1/beebright-local/main/'; +async function read(url){const response=await fetch(url);if(!response.ok)throw new Error('Local release download failed: '+response.status);return response.text();} +let manifest; +try{manifest=JSON.parse(await read('https://github.com/Dev-v1/beebright-local/releases/latest/download/update-manifest.json'));} +catch{manifest=JSON.parse(await read(repository+'update-manifest.json'));} +if(!/^[a-f0-9]{40}$/.test(manifest.version)||!/^[a-f0-9]{64}$/.test(manifest.sha256)||!/^https:\/\/github\.com\/Dev-v1\/beebright-local\/releases\/download\/desktop-[a-f0-9]{12}\/beebright-local-source\.zip$/.test(manifest.url))throw new Error('Invalid local release manifest.'); +await fs.mkdir(path.join(output,'local'),{recursive:true}); +await fs.writeFile(path.join(output,'local/manifest.json'),JSON.stringify(manifest)); +for(const name of ['install.ps1','install.sh','bootstrap.ps1','bootstrap.py']){ + const target=name.startsWith('bootstrap')?'local/'+name:name; + await fs.writeFile(path.join(output,target),await read(repository+name)); +} +console.log('Download links point to the independently published BeeBright Local release.'); diff --git a/tools/test_local_ui.py b/tools/test_local_ui.py index fe8867d..efd373a 100644 --- a/tools/test_local_ui.py +++ b/tools/test_local_ui.py @@ -35,7 +35,7 @@ def test(w): w.evaluate_js("(()=>{const e=document.querySelector('[aria-label=\"Neon Dash course\"]');e.value='4';e.dispatchEvent(new Event('change',{bubbles:true}));})()") w.evaluate_js("(()=>{const e=document.querySelector('[aria-label=\"Marble Run course\"]');e.value='2';e.dispatchEvent(new Event('change',{bubbles:true}));})()") assert w.evaluate_js("document.querySelector('[aria-label=\"Neon Dash course\"]').value==='4' && document.querySelector('[aria-label=\"Marble Run course\"]').value==='2'") - assert w.evaluate_js("document.querySelector('[aria-label=\"Neon Rally track\"]').options.length===12") + assert w.evaluate_js("document.querySelector('[aria-label=\"Neon Rally track\"]').options.length===4") w.evaluate_js("Array.from(document.querySelectorAll('.game-card')).find(c=>c.querySelector('h2').innerText==='Pocket Bowling').querySelector('button').click()") wait(w,"document.querySelector('.game-overlay .primary') !== null && !document.querySelector('.game-overlay .primary').disabled") w.evaluate_js("document.querySelector('.game-overlay .primary').click()") @@ -132,17 +132,15 @@ def test(w): w.evaluate_js("document.querySelector('.arcade-player-bar button').click()") wait(w,"document.querySelectorAll('.game-card').length===8") # Exercise every independent Rally track in the installed High quality UI. - track_names = w.evaluate_js("Array.from(document.querySelector('[aria-label=\"Neon Rally track\"]').options, o=>o.text.replace(/^\\d+\\. /,''))") - assert len(track_names) == 12 and len(set(track_names)) == 12 - for track, track_name in enumerate(track_names): - w.evaluate_js("(()=>{const s=document.querySelector('[aria-label=\"Race laps\"]');s.value='"+str(track%10+1)+"';s.dispatchEvent(new Event('change',{bubbles:true}));})()") + for track in range(4): + w.evaluate_js("(()=>{const s=document.querySelector('[aria-label=\"Race laps\"]');s.value='"+str(track+1)+"';s.dispatchEvent(new Event('change',{bubbles:true}));})()") w.evaluate_js("(()=>{const s=document.querySelector('[aria-label=\"Neon Rally track\"]');s.value='"+str(track)+"';s.dispatchEvent(new Event('change',{bubbles:true}));})()") w.evaluate_js("Array.from(document.querySelectorAll('.game-card')).find(c=>c.querySelector('h2').innerText==='Neon Rally').querySelector('button').click()") wait(w,"document.querySelector('.game-overlay .primary')!==null && !document.querySelector('.game-overlay .primary').disabled") w.evaluate_js("document.querySelector('.game-overlay .primary').click()") wait(w,"document.querySelector('.game-overlay')===null") time.sleep(.4) - assert w.evaluate_js("document.querySelector('.arcade-player-bar').innerText.includes("+repr(track_name)+")") + assert w.evaluate_js("document.querySelector('.arcade-player-bar').innerText.includes("+repr(['Harbor Circuit','Canyon Switchback','Alpine Hairpins','Midnight Metro'][track])+")") assert w.evaluate_js("/Place [1-5]\\/5/.test(document.querySelector('.arcade-player-bar').innerText) && document.querySelector('.arcade-player-bar').innerText.includes('Hill')") assert w.evaluate_js("document.querySelector('.arcade-player-bar').innerText.includes('Chase')") assert w.evaluate_js("document.querySelector('.arcade-player-bar').innerText.includes('Lap 1/"+str(track+1)+"')") @@ -186,7 +184,7 @@ def test(w): wait(w,"document.querySelector('.arcade-player-bar').innerText.includes('Lowering') || document.querySelector('.arcade-player-bar').innerText.includes('Crossing')") w.evaluate_js("document.querySelector('.arcade-player-bar button').click()") wait(w,"document.querySelectorAll('.game-card').length===8") - print('Twelve High quality Rally tracks and hold/release bridge controls passed.') + print('Four High quality Rally tracks and hold/release bridge controls passed.') # Leaving and resuming must retain the earned break and its original deadline. w.evaluate_js("document.querySelector('.brand').click()") wait(w,"document.querySelector('.hero-copy') !== null")