chore: initial commit

This commit is contained in:
Giancarmine Salucci
2026-03-31 18:38:34 +02:00
commit 3d1f3e5eb4
79 changed files with 6659 additions and 0 deletions

12
.claude/settings.json Normal file
View File

@@ -0,0 +1,12 @@
{
"mcpServers": {
"trueref": {
"command": "node",
"args": ["--import=tsx/esm", "/home/moze/Sources/trueref/src/mcp/index.ts"],
"env": {
"TRUEREF_API_URL": "http://localhost:5173"
}
}
},
"enableAllProjectMcpServers": true
}

View File

@@ -0,0 +1,72 @@
{
"permissions": {
"allow": [
"Read(//home/moze/Sources/trueref/**)",
"WebFetch(domain:en.wikipedia.org)",
"Read(//home/moze/.claude/**)",
"Read(//home/moze/.config/claude/**)",
"Bash(curl -s \"http://localhost:5173/api/v1/libs/search?libraryName=phaser&query=phaser+game+framework&type=txt\")",
"Skill(update-config)",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=load+texture+atlas+sprite+sheet+frame&type=txt&tokens=3000\")",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=scene+preload+create+game+config+setup&type=txt&tokens=3000\")",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=load+atlas+texture+atlas+json+this.load.atlas&type=txt&tokens=2000\")",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=image+draggable+input+pointer+drag+interactive&type=txt&tokens=2000\")",
"Bash(npm create:*)",
"Bash(npx:*)",
"Bash(echo {})",
"Bash(npm init:*)",
"Bash(npm install:*)",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=game+object+image+add+scene+this.add.image+create&type=txt&tokens=2000\")",
"Bash(cp /tmp/phaser-atlas.json /home/moze/Sources/phaser-scopa/public/atlas.json)",
"Bash(cp /home/moze/Immagini/Napoletane/dist/atlas.png /home/moze/Sources/phaser-scopa/public/atlas.png)",
"Bash(cp /home/moze/Immagini/Napoletane/dist/retro.png /home/moze/Sources/phaser-scopa/public/retro.png)",
"mcp__chrome-devtools__take_screenshot",
"mcp__chrome-devtools__click",
"mcp__chrome-devtools__evaluate_script",
"mcp__chrome-devtools__list_console_messages",
"mcp__chrome-devtools__list_network_requests",
"mcp__chrome-devtools__navigate_page",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=particle+emitter+ParticleEmitter+explode+burst+effect&type=txt&tokens=3000\")",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=WebAudioSound sound manager play background music loop&type=txt&tokens=2000\")",
"Bash(curl -v \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=particle+emitter+burst+effect&type=txt&tokens=2000\")",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=particle+emitter+burst+effect&type=txt&tokens=2000\" -o /tmp/trueref_particles.txt)",
"Read(//tmp/**)",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=sound+manager+audio+play+loop&type=txt&tokens=2000\" -o /tmp/trueref_audio.txt)",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=ParticleEmitter+addEmitter+createEmitter+gravity+speed&type=txt&tokens=2000\" -o /tmp/t1.txt)",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=tween+chain+timeline+sequence+onComplete&type=txt&tokens=2000\" -o /tmp/t2.txt)",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=add particles texture frame lifespan speed scale alpha&type=txt&tokens=3000\" -o /tmp/t3.txt)",
"Bash(curl -s --max-time 10 \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=graphics fillRoundedRect strokeRoundedRect radius corner&type=txt&tokens=2500\")",
"Bash(curl -s http://localhost:3000)",
"mcp__chrome-devtools__get_console_message",
"mcp__context7__resolve-library-id",
"Bash(curl -s http://localhost:5173/api/v1/libraries)",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=mobile+android+APK+cordova+capacitor+package&type=txt\")",
"Bash(curl -sv \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=mobile+android+APK+cordova+capacitor+package&type=txt\")",
"Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=device+mobile+scale+touch+input+browser&type=txt&tokens=3000\")",
"Bash(head -60 echo \"---\" curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=scale manager fit resize mobile device&type=txt&tokens=2000\")",
"Bash(java -version)",
"Read(//home/moze/Android/Sdk/**)",
"Bash(node -e \"require\\(''child_process''\\).execSync\\(''npx @capacitor/cli --version'', {stdio:''inherit''}\\)\")",
"Read(//usr/lib/android-sdk/**)",
"Read(//opt/**)",
"Read(//usr/**)",
"Bash(dpkg -l android-sdk)",
"Bash(adb version:*)",
"Bash(apt-cache search:*)",
"Bash(curl:*)",
"Bash(sudo apt-get:*)",
"Bash(unzip -q /tmp/cmdline-tools.zip -d ~/android-sdk/cmdline-tools)",
"Bash(mv ~/android-sdk/cmdline-tools/cmdline-tools ~/android-sdk/cmdline-tools/latest)",
"Read(//home/moze/android-sdk/cmdline-tools/latest/**)",
"Read(//home/moze/android-sdk/**)",
"Bash(export ANDROID_SDK_ROOT=$HOME/android-sdk export ANDROID_HOME=$HOME/android-sdk export PATH=$PATH:$HOME/android-sdk/cmdline-tools/latest/bin __NEW_LINE_061516952640df3a__ yes)",
"Bash($HOME/android-sdk/cmdline-tools/latest/bin/sdkmanager --sdk_root=$HOME/android-sdk \"platforms;android-34\" \"build-tools;34.0.0\")",
"Bash(npm run:*)",
"Bash(export ANDROID_SDK_ROOT=$HOME/android-sdk)",
"Bash(export ANDROID_HOME=$HOME/android-sdk)",
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(s[''''title''''], ''''|'''', s[''''description''''][:80]\\) for s in d.get\\(''''snippets'''',[]\\)]\")",
"Bash(grep -r \"android-sdk\\\\|ANDROID_SDK\\\\|sdk.dir\" /home/moze/Sources/phaser-scopa/android/ --include=*.properties --include=*.gradle --include=*.gradle.kts)",
"Bash(./gradlew assembleDebug --quiet)"
]
}
}

1
.github/agents vendored Symbolic link
View File

@@ -0,0 +1 @@
/home/moze/Sources/copilot-agents/.github/agents

1
.github/schemas vendored Symbolic link
View File

@@ -0,0 +1 @@
/home/moze/Sources/copilot-agents/.github/schemas

1
.github/skills vendored Symbolic link
View File

@@ -0,0 +1 @@
/home/moze/Sources/copilot-agents/.github/skills

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
prompts/
docs/docs_cache_state.yaml

225
TRUEREF_FEEDBACK.md Normal file
View File

@@ -0,0 +1,225 @@
# TrueRef Feedback — From a Phaser.js Development Session
**Date:** 2026-03-26
**Project:** Scopone Scientifico card game with Phaser 3.90
**Library Indexed:** `/local/phaser` (cloned Phaser repo, ~3.5M tokens, 8848 snippets)
---
## What Worked Well
### 1. Library Discovery
`resolve-library-id` found `/local/phaser` immediately:
```json
{"results":[{"id":"/local/phaser","title":"phaser","state":"finalized",
"totalTokens":3511693,"totalSnippets":8848}]}
```
Zero friction. The `state: "finalized"` field is useful for callers to know it's ready.
### 2. Relevant Snippets for Conceptual Queries
Broad queries like `"scene preload create game config setup"` and `"image draggable input pointer drag interactive"` returned well-matched `InputConfiguration.js` typedef and scene lifecycle documentation. The semantic search correctly ranked relevant fragments for natural-language queries.
### 3. Speed
All queries resolved in under 200ms against the local SQLite index. Excellent for tight dev loops.
### 4. Context7 Compatibility
The drop-in interface (same tool names, same params) worked as expected. No adapter needed.
### 5. Particle API Migration Docs — Well Indexed
Query `"add particles texture lifespan speed scale alpha"` correctly returned the full
`ParticleEmitter.js` JSDoc block explaining the v3.55→v3.60 API change (removal of
`ParticleEmitterManager`, new `this.add.particles(x,y,texture,config)` signature). This
was the most critical query of session 2 and it landed on exactly the right snippet.
### 6. Audio API Discovery
Query `"sound manager audio play loop"` returned `HTML5AudioSound.js` which confirmed
the `this.sound.context` path for accessing the raw `AudioContext`. Useful for procedural
audio patterns that don't need loaded assets.
---
## Issues Found
### BUG — FTS5 Special Character Crash
**Severity: High**
Any query containing a `.` (dot) causes a hard server error:
```
GET /api/v1/context?query=this.load.atlas → {"error":"fts5: syntax error near \".\"","code":"INTERNAL_ERROR"}
```
This is triggered by the most natural way to ask about Phaser APIs — developers instinctively include method names like `this.load.atlas()`, `Phaser.Scale.FIT`, `scene.add.image()`.
**Root cause:** FTS5 treats `.` as a query operator. The user query string is passed raw into the FTS5 `MATCH` expression.
**Fix:**
```typescript
// In query-docs handler, sanitize for FTS5 before keyword search
function sanitizeFts5(query: string): string {
// Remove/replace FTS5 special chars: . , ( ) " * ^ ~ - + @ =
return query.replace(/[.()"*^~\-+@=,]/g, ' ').replace(/\s+/g, ' ').trim();
}
```
Or better: wrap the whole query in `"..."` for phrase matching, or switch to semantic-only when the query contains code-like symbols.
---
### BUG — No Automatic Fallback on FTS5 Error
**Severity: Medium**
When keyword search fails (FTS5 crash, empty results), the server returns an error or empty response instead of automatically retrying with `searchMode=semantic`. Given that `searchMode=auto` is the default, "auto" should truly mean "try keyword, fall back to semantic if needed."
**Current behaviour:** `searchMode=auto` → keyword fails → 500 error
**Expected behaviour:** `searchMode=auto` → keyword fails → retry with semantic → return best result
---
### USABILITY — Semantic Search Imprecision for API Surface Queries
**Severity: Medium**
Query: `"load atlas texture atlas json this.load.atlas"`
Result: Returned `SpriteSheetFile.js` docs (adjacent topic, wrong API).
The correct result would be `AtlasJSONFile.js` with the `this.load.atlas()` signature. This may be a chunking issue — if the atlas loader code isn't semantically close to "load atlas texture atlas json", the embedding is misleading.
**Suggestion:** Index JSDoc `@method` names and their parameter descriptions as separate high-weight chunks, so API surface queries get precise matches even when the function body is minimal.
---
### USABILITY — No Symbol/API Lookup Mode
**Severity: LowMedium**
There's no way to say "give me the signature and docs for `Phaser.Loader.LoaderPlugin#atlas`" without knowing what file it lives in. Context7 has the same limitation but a "symbol search" mode would be very useful for:
- Finding the exact overloads of a method
- Looking up TypeDef shapes
- Checking which config properties exist on a class
**Suggested addition:**
```
GET /api/v1/symbol?libraryId=...&symbol=LoaderPlugin%23atlas
```
Or as a `searchMode=symbol` option in the existing `/context` endpoint.
---
### USABILITY — Text Output Lacks File-Path Headers for Some Snippets
**Severity: Low**
Some returned snippet blocks lack the `### FileName.js\n*src/path/to/file.js*` header, making it hard to know which part of the codebase a snippet comes from. Consistent headers on every snippet would help developers navigate to the source.
---
### USABILITY — `tokens` Parameter Has No Visible Effect Below ~3000
**Severity: Low**
Setting `tokens=500` still returned >1500 tokens of content. The parameter seems to be a soft cap or not enforced strictly. Clarify in the docs whether it truncates at the snippet level or the total response level, and enforce it more precisely so callers can budget context window usage.
---
### BUG — Empty Response Instead of "No Results" for Some Queries
**Severity: Medium**
Several queries returned a `200 OK` with an empty body (0 bytes):
- `"particle emitter ParticleEmitter explode burst effect"` → 0 bytes
- `"ParticleEmitter addEmitter createEmitter gravity speed"` → 0 bytes
The identical concept with different phrasing (`"add particles texture lifespan speed scale alpha"`) returned 3451 bytes of perfect content. This means:
1. The FTS5/semantic pipeline silently returns nothing for some query formulations
2. The caller has no way to distinguish "no results" from a server error
3. A 0-byte 200 response should instead be a `{"message": "no matching snippets"}` with a 404, or the `auto` mode should try rephrasing before giving up
**Fix:** Return a structured "no results" response body even when the result set is empty. Consider adding a `suggestions` field with alternative query terms.
---
### BUG — camelCase Terms Return Zero Results (Keyword + Semantic)
**Severity: High**
Queries using camelCase method names return 0 snippets even in `searchMode=semantic`:
```
query=lockOrientation+landscape → {"snippets":[],"totalTokens":0}
query=screen+orientation+landscape+portrait+lock+mobile+device&searchMode=semantic → {"snippets":[],"totalTokens":0}
```
The word "orientation" and "landscape" clearly appear in `ScaleManager.js` (confirmed by direct keyword search returning the snippet). The semantic embeddings are not matching natural-language descriptions of the API to actual occurrences.
**Root cause:** `lockOrientation` is only mentioned as JSDoc `@method` header with minimal surrounding prose. The embedding vector is too sparse to match broader concept queries. A dedicated method-summary chunk (method name + params + one-line description) would dramatically improve recall.
**Fix:** During indexing, extract each `@method` block as its own mini-chunk: `"{methodName}({params}): {description}"`. This gives the embedder enough signal to match `"lock screen to landscape"``lockOrientation(orientation)`.
---
### FINDING — Library Resolution Returns Real context7, Not Local trueref
**Severity: Medium / Workflow**
When the project configures trueref as a local MCP server (`mcpServers.trueref`), the Claude Code MCP tool namespace resolves to `mcp__context7__*` — the same namespace as the cloud context7 service. This causes **silent fallthrough to the wrong backend**: `resolve-library-id("phaser")` returned `/phaserjs/phaser` (cloud) instead of `/local/phaser` (trueref local).
**Impact:** Developers who configure both context7 (global) and trueref (project) have no way to tell which backend answered without inspecting the library IDs returned. The local trueref advantage (exact-version index) is silently bypassed.
**Fix suggestions:**
- Trueref MCP server should expose a distinct tool namespace (e.g., `trueref__query-docs`) OR
- Return a `"source": "local"` field in every response so callers can verify they're hitting the local index
- Document that the project MCP server name should differ from `context7` to avoid namespace collision
---
### FINDING — APK/Mobile Packaging Docs Absent from Index
**Severity: Low (expected gap)**
Queries about mobile packaging (`android APK cordova capacitor package build export`) return 0 snippets. This is expected — the Phaser source repo is a game framework, not a packaging tool. However, Phaser's documentation website (`https://phaser.io/tutorials/making-your-first-phaser-3-game`) does include Cordova/Capacitor guides.
**Suggestion:** When trueref indexes a library, optionally allow indexing the companion **documentation site** as a secondary source alongside the source repo. The `/local/phaser` index could be augmented with phaser.io/tutorials content, giving results for "how to package as APK" that the source code alone cannot provide.
---
## Summary Table
| Issue | Severity | Type |
|-------|----------|------|
| FTS5 crash on dots/special chars | High | Bug |
| camelCase terms return 0 results (keyword+semantic) | High | Bug |
| No fallback from keyword → semantic | Medium | Bug |
| Library namespace collision (local vs cloud context7) | Medium | Workflow Bug |
| Imprecise semantic results for API names | Medium | Usability |
| No symbol/method lookup mode | Medium | Feature Request |
| Empty body instead of structured "no results" | Medium | Bug |
| Missing file-path headers on some snippets | Low | Usability |
| `tokens` soft cap not enforced | Low | Usability |
| No docs-site indexing (packaging/tutorial content) | Low | Feature Request |
---
## Suggested Priority
1. Fix the FTS5 special-char crash (blocks real-world usage immediately)
2. Fix camelCase zero-results (method-summary mini-chunks at index time)
3. Auto-fallback in `searchMode=auto`
4. Disambiguate local vs cloud namespace in MCP tool names
5. Improve API surface indexing (separate JSDoc chunks with method names)
6. Symbol lookup endpoint
---
## Notes on the Development Experience
### Sessions 12 (Game development)
Using trueref for a local Phaser clone was smooth once queries were phrased as natural language rather than code expressions. The semantic search accurately returned `InputConfiguration` docs for "draggable input pointer", and correctly found the v3.55→v3.60 `ParticleEmitter` API migration notes. `camera.postFX.addVignette`, `fillRoundedRect`, `postFX.addGlow`, `camera.shake`, `camera.flash` — all found correctly.
### Session 3 (Packaging to APK)
Attempted to use trueref to guide Android APK packaging via Capacitor. All mobile/orientation queries returned empty:
- `lockOrientation landscape` → 0 snippets
- `screen orientation landscape portrait lock mobile device` (semantic) → 0 snippets
The `ScaleManager.lockOrientation` method IS in the index (it appeared in a broader earlier query), but no orientation-specific query could retrieve it. This is the camelCase / sparse-JSDoc problem documented above.
**Packaging was completed via Capacitor without trueref assistance:**
```
npm install @capacitor/core @capacitor/cli @capacitor/android
npx cap init "Scopone Scientifico" "com.phaser.scopa" --web-dir dist
npx cap add android
# Set android:screenOrientation="landscape" in AndroidManifest.xml
./gradlew assembleDebug
# → android/app/build/outputs/apk/debug/app-debug.apk (15MB)
```
The key advantage over context7 for private codebases remains obvious: indexing a local clone gives docs matching the exact version in use. Fix the FTS5 crash, the camelCase recall gap, and the namespace collision and it becomes fully production-ready.

101
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

54
android/app/build.gradle Normal file
View File

@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace = "com.phaser.scopa"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.phaser.scopa"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,5 @@
package com.phaser.scopa;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Scopone Scientifico</string>
<string name="title_activity_main">Scopone Scientifico</string>
<string name="package_name">com.phaser.scopa</string>
<string name="custom_url_scheme">com.phaser.scopa</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

29
android/build.gradle Normal file
View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

22
android/gradle.properties Normal file
View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

5
android/settings.gradle Normal file
View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

16
android/variables.gradle Normal file
View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}

9
capacitor.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.phaser.scopa',
appName: 'Scopone Scientifico',
webDir: 'dist'
};
export default config;

147
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,147 @@
# Architecture
> Last Updated: 2026-03-31T00:00:00.000Z
## Overview
| Attribute | Value |
|-----------------|--------------------------------------------------------|
| **Language** | TypeScript (ES2020 target, strict mode) |
| **Type** | 2D card game — Scopone Scientifico |
| **Framework** | Phaser 3.87+ (scene-based game engine) |
| **Bundler** | Vite 5 |
| **Native** | Capacitor 8.3 (Android) |
| **Resolution** | 1280 × 720, FIT scaling with auto-center |
## Project Structure
```
scopone-phaser/
├── src/
│ ├── main.ts # Phaser.Game bootstrap + config
│ ├── game/
│ │ ├── types.ts # Card, Suit, Player, GameState, TeamScore, ScoreBreakdown, PRIMIERA_VALUES
│ │ ├── engine.ts # Deck build, shuffle, capture logic, applyMove, scoring, primiera
│ │ └── ai.ts # Heuristic AI: chooseMove, scoreCapture, scoreDump
│ └── scenes/
│ ├── BootScene.ts # Asset loading (atlas, card back image)
│ ├── MenuScene.ts # Start menu with rules summary
│ └── GameScene.ts # Main game: rendering, turn management, effects, audio
├── index.html # Entry point, Italian locale, green felt background
├── public/ # Static assets (atlas.json, atlas.png, retro.png)
├── android/ # Capacitor Android native shell
├── package.json
├── tsconfig.json
├── vite.config.ts
└── capacitor.config.ts
```
## Key Directories
| Directory | Purpose |
|--------------------|----------------------------------------------------------------|
| `src/game/` | Domain logic — types, game engine, AI (no Phaser dependency) |
| `src/scenes/` | Phaser scenes — rendering, input, effects, audio |
| `public/` | Static assets served by Vite (card atlas, card back) |
| `android/` | Capacitor-generated Android project (Gradle, Java) |
| `prompts/` | JIRA agent pipeline artifacts |
## Design Patterns
| Pattern | Where |
|-----------------------------|--------------------------------------------------------------------|
| **Scene lifecycle** | BootScene → MenuScene → GameScene (Phaser scene graph) |
| **Immutable state updates** | `applyMove()` deep-clones `GameState` before mutation |
| **Heuristic scoring** | AI evaluates all legal moves with weighted feature scores |
| **Separation of concerns** | `game/` has no Phaser imports; `scenes/` bridges game ↔ rendering |
| **Procedural audio** | Web Audio API oscillators + delay reverb — no audio files |
No explicit GoF patterns (singleton, factory, observer, DI) detected.
## Key Components
### `types.ts` — Domain Types
- `Card { suit, value, id }` — 40-card Napoletane deck (suits: bastoni, coppe, denara, spade; values 1-10)
- `GameState` — full round state: 4 players, table, current player, team scores, round tracking
- `TeamScore` — per-team stats: cards, scope, denari, settebello, primiera, round/total points
- `ScoreBreakdown` — which team won each scoring category
- `PRIMIERA_VALUES` — lookup table for primiera card values
### `engine.ts` — Game Logic
- `buildDeck()` / `shuffle()` — Fisher-Yates 40-card deck
- `createInitialState()` — deals 10 cards per player, empty table (Scopone Scientifico rules)
- `findCaptures(played, table)` — direct value match (mandatory) or subset-sum combinations
- `applyMove(state, player, card, captureChoice)` — immutable state transition, scopa detection, end-of-round scoring
- `calculateScores()` / `scoreRound()` — carte, denari, settebello, primiera, scope points
- `calcPrimiera(pile)` — best card per suit using `PRIMIERA_VALUES`
- `teamOf(playerIdx)` — team assignment: 0+2 = Team A, 1+3 = Team B
### `ai.ts` — Heuristic AI
- `chooseMove(state, playerIdx)` — evaluates all legal moves (captures + dumps)
- `scoreCapture()` — weighted: scopa (+500), settebello (+300), denari (+50 each), card count, primiera value, opponent threat
- `scoreDump()` — avoids giving opponents scopa (-400), prefers low-value non-denari, penalises dumping 7s and aces
### `GameScene.ts` — Main Scene (~1340 lines)
- Four-player layout: South (human), West/East (AI, rotated ±90°), North (AI partner)
- Deal animation with staggered tweens
- Card selection with postFX glow pulse
- Capture highlighting with multiple-choice UI
- Particle effects: capture burst, scopa explosion, settebello flash, denari shimmer, primiera glow, card trails, victory confetti
- Camera shake + flash on scopa and settebello
- Live score bar with animated counter updates
- Think bar progress indicator during AI turns
- Procedural background music (oscillator drone + triangle melody + chord stabs)
- Round-end summary panel and game-over screen
## Dependencies
### Production
| Package | Version | Purpose |
|----------------------|----------|--------------------------------------|
| `phaser` | ^3.87.0 | 2D game engine |
| `@capacitor/core` | ^8.3.0 | Capacitor runtime |
| `@capacitor/cli` | ^8.3.0 | Capacitor CLI |
| `@capacitor/android` | ^8.3.0 | Android platform plugin |
### Development
| Package | Version | Purpose |
|---------------|---------|--------------------------|
| `typescript` | ^5.0.0 | TypeScript compiler |
| `vite` | ^5.0.0 | Dev server and bundler |
## Module Organisation
```
main.ts ──→ BootScene ──→ MenuScene ──→ GameScene
├── game/engine (createInitialState, applyMove, findCaptures, ...)
├── game/ai (chooseMove)
└── game/types (Card, GameState, ...)
```
`game/` modules are pure logic with no framework coupling. `scenes/` imports from `game/` but never vice versa.
## Data Flow
1. `createInitialState()` builds shuffled deck, deals 10 cards each, empty table
2. `GameScene.nextTurn()` detects current player: human → enable input; AI → delay + `chooseMove()`
3. `applyMove()` returns new `GameState` + capture result + scopa flag
4. `GameScene.executeMove()` animates: card flight → capture burst → pile collection
5. `updateScoreBar()` reflects live team stats with animated counter tweens
6. When all hands empty → `calculateScores()` → round-end overlay
7. First team to 11 points → game-over screen → optional restart
## Build System
| Command | Action |
|--------------------|--------------------------------------------|
| `npm run dev` | `vite` — dev server on port 3000 |
| `npm run build` | `tsc && vite build` — compile + bundle to `dist/` |
| `npm run preview` | `vite preview` — preview production build |
| `tsc --noEmit` | Type-check only (no test framework) |

145
docs/CODE_STYLE.md Normal file
View File

@@ -0,0 +1,145 @@
# Code Style
> Last Updated: 2026-03-31T00:00:00.000Z
## Language & Version
- **TypeScript** 5.x, strict mode enabled
- Target: **ES2020**, module: **ESNext**, module resolution: **bundler**
- `noEmit: true` — Vite handles transpilation; `tsc` is used for type-checking only
## Naming Conventions
| Kind | Convention | Examples |
|------------------|---------------|-------------------------------------------------------------------|
| Types/Interfaces | PascalCase | `Card`, `GameState`, `PlayerIndex`, `TeamScore`, `ScoreBreakdown` |
| Type aliases | PascalCase | `Suit`, `PlayerIndex` |
| Classes | PascalCase | `BootScene`, `MenuScene`, `GameScene` |
| Functions | camelCase | `buildDeck()`, `findCaptures()`, `chooseMove()`, `applyMove()` |
| Constants | UPPER_SNAKE | `PRIMIERA_VALUES`, `SUITS`, `AI_DELAY`, `SCOREBAR_H` |
| Local variables | camelCase | `bestMove`, `capturedCards`, `isScopa`, `afterTable` |
| Private members | camelCase | `this.state`, `this.cardImages`, `this.aiThinking` |
| Parameters | camelCase | `playerIdx`, `captureChoice`, `onComplete` |
## Class & Scene Patterns
Scenes extend `Phaser.Scene` and follow the Phaser lifecycle:
```ts
export class BootScene extends Phaser.Scene {
constructor() {
super({ key: 'BootScene' });
}
preload(): void { /* asset loading */ }
create(): void { /* scene setup */ }
}
```
Scene registration via `Phaser.Types.Core.GameConfig.scene` array in `main.ts`.
## Indentation & Formatting
- **2-space** indentation
- **Single quotes** for string literals
- No semicolons omission — **semicolons used consistently**
- Trailing commas in multi-line objects/arrays
- `const` preferred; `let` when reassignment is needed; no `var`
## Import Patterns
Named imports from local modules:
```ts
import { Card, GameState, PlayerIndex } from './types';
import { findCaptures, canCapture, calcPrimiera, teamOf } from './engine';
import { chooseMove } from '../game/ai';
```
Default import for Phaser:
```ts
import Phaser from 'phaser';
```
Type-only import for Capacitor config:
```ts
import type { CapacitorConfig } from '@capacitor/cli';
```
Relative paths only (`./`, `../`). No path aliases configured.
## Export Patterns
- **Named exports** for all public symbols (`export function`, `export class`, `export interface`, `export type`, `export const`)
- No default exports except `vite.config.ts` and `capacitor.config.ts` (framework convention)
- Private/internal functions are not exported (`getSubsets`, `calculateScores`, `scoreRound`, `deepClone`)
## Type Annotations
- Explicit return types on exported functions: `(): Card[]`, `(): GameState`, `(): boolean`
- Union types for constrained values: `PlayerIndex = 0 | 1 | 2 | 3`
- String literal unions: `Suit = 'bastoni' | 'coppe' | 'denara' | 'spade'`
- Tuple types for fixed-length arrays: `[Player, Player, Player, Player]`, `[TeamScore, TeamScore]`
- `Record<K, V>` for maps: `PRIMIERA_VALUES: Record<number, number>`
## Comments & Documentation
- **JSDoc** comments on key exported functions (`findCaptures`, `applyMove`)
- Section separators using dashed lines:
```ts
// ---------------------------------------------------------------------------
// Deck
// ---------------------------------------------------------------------------
```
- `[trueref]` annotations documenting Phaser API provenance
- Inline comments for non-obvious logic (capture rules, AI heuristic weights)
- No auto-generated docs or separate documentation tooling
## Code Examples (from codebase)
### Immutable state update pattern
```ts
export function applyMove(
state: GameState,
playerIdx: PlayerIndex,
card: Card,
captureChoice?: Card[]
): { nextState: GameState; capture: Capture | null; isScopa: boolean } {
const state2 = deepClone(state);
// ... mutations on state2 ...
return { nextState: state2, capture: ..., isScopa };
}
```
### AI heuristic weighted scoring
```ts
function scoreCapture(...): number {
let score = 100; // base for capturing anything
if (isScopa) score += 500;
if (settebello) score += 300;
score += denariCount * 50;
score += captured.length * 20;
// ...
return score;
}
```
### Phaser particle effect
```ts
const e1 = this.add.particles(x, y, 'particle_glow', {
lifespan: { min: 350, max: 700 },
speed: { min: 80, max: 280 },
scale: { start: 0.9, end: 0 },
tint: color, gravityY: 100, emitting: false,
}).setDepth(25);
e1.explode(count);
```
## Linting & Formatting
No ESLint or Prettier configuration detected. Code style is maintained manually.
TypeScript strict mode provides type-level linting (`strict: true` in `tsconfig.json`).

99
docs/FINDINGS.md Normal file
View File

@@ -0,0 +1,99 @@
# Findings
> Last Updated: 2026-03-31T00:00:00.000Z
## Summary
Initial analysis of the Scopone Scientifico Phaser 3 codebase. This document is populated by the Planner agent as research is performed.
## Codebase Observations
- **Total source files**: 9 TypeScript (6 in `src/`), 3 Java (Capacitor boilerplate)
- **Largest file**: `GameScene.ts` (~1340 lines) — rendering, input, effects, audio, UI
- **Game logic is framework-independent**: `game/` modules have zero Phaser imports
- **No test framework**: only `tsc --noEmit` for type-checking
- **No linter/formatter**: code style enforced manually
- **AI plays all 3 non-human seats** using the same heuristic
- **Procedural audio**: all sound is Web Audio oscillators — no audio asset files
## Potential Improvement Areas
- **AI cheats with perfect information**: `scoreDump()` and `opponentThreatScore()` in `ai.ts` iterate `opp.hand` directly — bots can see all opponent cards. Must be replaced with imperfect-information card tracking.
- **No mastery/difficulty levels**: All 3 AI seats use the same heuristic at the same strength.
- **No card tracking**: No module tracks which cards have been played or remains in the deck.
- **No minimax**: Pure heuristic scoring, no look-ahead or game tree search.
- **Allied bot is selfish**: Compagno (player 2) plays identically to opponents — no cooperative strategy.
## Research Performed
### Web Research: Scopone Scientifico Rules (2026-03-31)
**Sources**: Wikipedia (*Scopa* article, Scopone section), Pagat.com (*Scopone* page by John McLeod)
#### Core Rules (Scopone Scientifico variant)
- 4 players, 2 fixed teams of 2 (sit opposite): Team A = players 0+2, Team B = players 1+3
- 40-card Napoletane deck: 4 suits (bastoni, coppe, denara, spade), values 110
- **All 40 cards dealt** (10 each), **no initial table cards** — the "scientifico" variant
- Play passes around the table (counter-clockwise in Italian tradition; this game uses 0→1→2→3)
- Each turn: play one card face-up to the table
#### Capture Rules
1. If the played card's value **matches a table card**, the table card **must** be captured (single card, not a sum)
2. If **multiple table cards match** the played value, exactly one is captured (player chooses)
3. If **no direct match**, the player may capture a **subset of table cards summing** to the played value
4. If the played card matches both a single card and a sum, **the single card must be captured** (not the sum)
5. There is **no obligation to play a capturing card** — a player may choose to play a non-capturing card instead. But if the played card CAN capture, it MUST capture.
6. **Scopa**: capturing ALL remaining table cards awards +1 point (except on the very last card of the round)
#### Scoring (per round, 4 fixed points + scope)
| Category | Rule |
|------------|----------------------------------------------------------|
| **Carte** | Team with majority of captured cards (20+ of 40). Tie = no point. |
| **Denari** | Team with majority of coins/denara suit cards (6+ of 10). Tie = no point. |
| **Settebello** | Team capturing the 7 of denara. Always awarded. |
| **Primiera** | Team with highest prime value. Prime = best card per suit using special scale. Tie = no point. Must have all 4 suits. |
| **Scope** | +1 per scopa achieved during play. |
#### Primiera Values (confirmed matching codebase)
| Card value | Primiera value |
|------------|---------------|
| 7 | 21 |
| 6 | 18 |
| 1 (Ace) | 16 |
| 5 | 15 |
| 4 | 14 |
| 3 | 13 |
| 2 | 12 |
| 8,9,10 | 10 |
A team missing an entire suit **cannot win primiera** (even 3×21=63 loses to 21+16+16+16=69 with all 4 suits).
#### Winning
- First team to **11+ points** at the end of a round wins
- If both reach 11 in the same round, higher total wins; if tied, play continues
#### Strategy Notes (from Pagat.com)
- **7 of coins (settebello)** is the single most valuable card — contributes to all 4 fixed scoring categories
- **Avoid giving scope**: leave table total ≥ 11 when possible
- **Anchor strategy**: leave a card on table that your team controls (you hold duplicates of that value)
- **Whirlwind**: consecutive scope — clearing the table forces opponent to play, partner captures, repeat
- **Sevens > sixes > aces** in priority for primiera control
- **Paired/unpaired tracking**: if all captures are single-card matches, the last card matches the last table card. Sum captures disrupt this pattern, important for end-game planning.
### Codebase Capture Rule Validation
The `findCaptures()` in `engine.ts` correctly implements:
- Direct match priority over sum captures ✓
- Multiple direct matches: takes ALL matching cards (slight deviation — pagat.com says choose ONE, but Wikipedia says take all. The codebase takes all direct matches. This is the **existing behavior** and must not be altered per success criteria.)
- Sum subsets via power set enumeration ✓
- `applyMove()` auto-captures when possible ✓
### Minimax Feasibility Analysis
- 10 cards per player × 4 players = 40 total moves per round
- Full game tree: ~10^12 nodes — infeasible for exhaustive search
- **Approach**: Depth-limited alpha-beta with determinization for imperfect information
- Sample N possible opponent hand assignments consistent with card tracking
- Run minimax on each sample to limited depth (46 plies)
- Average/vote across samples for best move
- Alpha-beta pruning reduces effective branching factor significantly
- Depth 4 (one full rotation) with ~5 moves per player = ~625 nodes per sample — very manageable
- 1020 samples × 625 nodes = ~6,00012,500 evaluations — runs in <100ms on modern hardware

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scopone Scientifico</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a5c2a; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<div id="game"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2081
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "phaser-scopa",
"version": "1.0.0",
"description": "Scopone Scientifico card game built with Phaser 3",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^5.0.0"
},
"dependencies": {
"@capacitor/android": "^8.3.0",
"@capacitor/cli": "^8.3.0",
"@capacitor/core": "^8.3.0",
"phaser": "^3.87.0"
}
}

808
public/atlas.json Normal file
View File

@@ -0,0 +1,808 @@
{
"frames": {
"bastoni_1": {
"frame": {
"x": 1620,
"y": 2588,
"w": 402,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 402,
"h": 645
},
"sourceSize": {
"w": 402,
"h": 645
}
},
"bastoni_10": {
"frame": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_2": {
"frame": {
"x": 405,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_3": {
"frame": {
"x": 810,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_4": {
"frame": {
"x": 1215,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_5": {
"frame": {
"x": 0,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_6": {
"frame": {
"x": 405,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_7": {
"frame": {
"x": 810,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_8": {
"frame": {
"x": 1215,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"bastoni_9": {
"frame": {
"x": 1620,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_1": {
"frame": {
"x": 2024,
"y": 2588,
"w": 402,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 402,
"h": 645
},
"sourceSize": {
"w": 402,
"h": 645
}
},
"coppe_10": {
"frame": {
"x": 1620,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_2": {
"frame": {
"x": 0,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_3": {
"frame": {
"x": 405,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_4": {
"frame": {
"x": 810,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_5": {
"frame": {
"x": 1215,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_6": {
"frame": {
"x": 1620,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_7": {
"frame": {
"x": 2025,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_8": {
"frame": {
"x": 2025,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"coppe_9": {
"frame": {
"x": 2025,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_1": {
"frame": {
"x": 2428,
"y": 2588,
"w": 402,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 402,
"h": 645
},
"sourceSize": {
"w": 402,
"h": 645
}
},
"denara_10": {
"frame": {
"x": 2430,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_2": {
"frame": {
"x": 2430,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_3": {
"frame": {
"x": 2430,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_4": {
"frame": {
"x": 0,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_5": {
"frame": {
"x": 405,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_6": {
"frame": {
"x": 810,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_7": {
"frame": {
"x": 1215,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_8": {
"frame": {
"x": 1620,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"denara_9": {
"frame": {
"x": 2025,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_1": {
"frame": {
"x": 2832,
"y": 2588,
"w": 402,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 402,
"h": 645
},
"sourceSize": {
"w": 402,
"h": 645
}
},
"spade_10": {
"frame": {
"x": 2430,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_2": {
"frame": {
"x": 2835,
"y": 0,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_3": {
"frame": {
"x": 2835,
"y": 647,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_4": {
"frame": {
"x": 2835,
"y": 1294,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_5": {
"frame": {
"x": 2835,
"y": 1941,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_6": {
"frame": {
"x": 0,
"y": 2588,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_7": {
"frame": {
"x": 405,
"y": 2588,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_8": {
"frame": {
"x": 810,
"y": 2588,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
},
"spade_9": {
"frame": {
"x": 1215,
"y": 2588,
"w": 403,
"h": 645
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 403,
"h": 645
},
"sourceSize": {
"w": 403,
"h": 645
}
}
},
"meta": {
"image": "atlas.png",
"scale": "1"
}
}

BIN
public/atlas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

BIN
public/retro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

163
src/game/ai.ts Normal file
View File

@@ -0,0 +1,163 @@
import { Card, GameState, PlayerIndex } from './types';
import { findCaptures, canCapture, calcPrimiera, teamOf } from './engine';
export interface AIMove {
card: Card;
capture: Card[];
}
/**
* Heuristic AI for Scopone Scientifico.
* Evaluates moves by scoring the value of captured cards.
*/
export function chooseMove(state: GameState, playerIdx: PlayerIndex): AIMove {
const player = state.players[playerIdx];
const hand = player.hand;
const table = state.table;
const myTeam = teamOf(playerIdx);
let bestMove: AIMove | null = null;
let bestScore = -Infinity;
for (const card of hand) {
const captures = findCaptures(card, table);
if (captures.length > 0) {
for (const captureSet of captures) {
const score = scoreCapture(card, captureSet, table, state, myTeam);
if (score > bestScore) {
bestScore = score;
bestMove = { card, capture: captureSet };
}
}
} else {
// No capture — score the "dump" move
const score = scoreDump(card, table, state, myTeam);
if (score > bestScore) {
bestScore = score;
bestMove = { card, capture: [] };
}
}
}
return bestMove!;
}
function scoreCapture(
played: Card,
captured: Card[],
table: Card[],
state: GameState,
myTeam: 0 | 1
): number {
let score = 100; // base for capturing anything
const allCaptured = [played, ...captured];
const afterTable = table.filter(c => !captured.some(cc => cc.id === c.id));
const isScopa = afterTable.length === 0;
// Scopa is very valuable
if (isScopa) score += 500;
// Settebello
if (allCaptured.some(c => c.suit === 'denara' && c.value === 7)) score += 300;
// Table has settebello and this capture takes it
if (table.some(c => c.suit === 'denara' && c.value === 7) &&
captured.some(c => c.suit === 'denara' && c.value === 7)) score += 200;
// Denari cards
score += allCaptured.filter(c => c.suit === 'denara').length * 50;
// More cards = better
score += captured.length * 20;
// High-value primiera cards
score += allCaptured.reduce((s, c) => s + primieraScore(c), 0);
// Avoid leaving settebello on table for opponent
const settebelloOnTable = afterTable.some(c => c.suit === 'denara' && c.value === 7);
if (settebelloOnTable) score -= 150;
// Opponent could easily capture remaining table
score -= opponentThreatScore(afterTable, state, myTeam) * 30;
return score;
}
function scoreDump(
card: Card,
table: Card[],
state: GameState,
myTeam: 0 | 1
): number {
let score = 0;
// Don't dump cards that complete opponent captures
const afterTable = [...table, card];
// Check if opponent can make a scopa after this dump
const opponentTeam = myTeam === 0 ? 1 : 0;
const opponentPlayers = state.players.filter(p => teamOf(p.index) === opponentTeam);
for (const opp of opponentPlayers) {
for (const oppCard of opp.hand) {
const caps = findCaptures(oppCard, afterTable);
for (const cap of caps) {
const leftAfter = afterTable.filter(c => !cap.some(cc => cc.id === c.id));
if (leftAfter.length === 0) score -= 400; // would give opponent scopa
}
}
}
// Prefer to dump low-value non-denari cards
if (card.suit !== 'denara') score += 30;
if (card.suit === 'denara') score -= 40;
if (card.suit === 'denara' && card.value === 7) score -= 300; // never dump settebello
// Prefer dumping face cards (10, 9, 8) that are less useful
if (card.value >= 8) score += 10;
// Don't dump 7s (primiera value)
if (card.value === 7) score -= 50;
// Don't dump 1s (primiera value)
if (card.value === 1) score -= 30;
// Penalty for making easy captures for opponents
const capturable = state.players
.filter(p => teamOf(p.index) !== myTeam)
.flatMap(p => p.hand)
.some(oppCard => canCapture(oppCard, afterTable));
if (capturable) score -= 20;
return score;
}
function primieraScore(card: Card): number {
// Reward for capturing high-primiera cards
const vals: Record<number, number> = { 7: 8, 6: 6, 1: 5, 5: 4, 4: 3, 3: 2, 2: 1 };
return vals[card.value] ?? 0;
}
function opponentThreatScore(
table: Card[],
state: GameState,
myTeam: 0 | 1
): number {
const opponentTeam = myTeam === 0 ? 1 : 0;
let threat = 0;
for (const player of state.players) {
if (teamOf(player.index) !== opponentTeam) continue;
for (const card of player.hand) {
const caps = findCaptures(card, table);
if (caps.length > 0) {
threat += caps[0].length;
// Extra threat if settebello is capturable
if (table.some(c => c.suit === 'denara' && c.value === 7) &&
caps.some(cap => cap.some(c => c.suit === 'denara' && c.value === 7))) {
threat += 5;
}
}
}
}
return threat;
}

307
src/game/engine.ts Normal file
View File

@@ -0,0 +1,307 @@
import {
Card, Suit, SUITS, Player, PlayerIndex, GameState,
TeamScore, ScoreBreakdown, PRIMIERA_VALUES, Capture
} from './types';
// ---------------------------------------------------------------------------
// Deck
// ---------------------------------------------------------------------------
export function buildDeck(): Card[] {
const deck: Card[] = [];
for (const suit of SUITS) {
for (let v = 1; v <= 10; v++) {
deck.push({ suit, value: v, id: `${suit}_${v}` });
}
}
return deck;
}
export function shuffle<T>(arr: T[]): T[] {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// ---------------------------------------------------------------------------
// Capture logic
// ---------------------------------------------------------------------------
/**
* Find all valid capture combinations for a played card against the table.
* Rules:
* - If the table contains a card of the same value, you MUST take it (and only it)
* unless the table has multiple cards of that value (take exactly one)
* - If no direct match exists, you may take any subset of table cards that sums to the played value
* Returns array of capture sets (each is a list of cards taken from table).
*/
export function findCaptures(played: Card, table: Card[]): Card[][] {
const results: Card[][] = [];
// Check for direct value matches
const directMatches = table.filter(c => c.value === played.value);
if (directMatches.length > 0) {
// Must capture exactly one matching card (Italian rules: take one direct match)
// Actually: if there's one direct match take it; if multiple, still take all that match
// Standard Italian rule: take ALL cards of matching value
results.push([...directMatches]);
return results; // direct match takes priority, no sum captures allowed
}
// No direct matches — find subsets that sum to played.value
const subsets = getSubsets(table);
for (const subset of subsets) {
if (subset.length >= 2) {
const sum = subset.reduce((acc, c) => acc + c.value, 0);
if (sum === played.value) {
results.push(subset);
}
}
}
return results;
}
function getSubsets(cards: Card[]): Card[][] {
const result: Card[][] = [[]];
for (const card of cards) {
const newSubsets = result.map(s => [...s, card]);
result.push(...newSubsets);
}
return result;
}
export function canCapture(played: Card, table: Card[]): boolean {
return findCaptures(played, table).length > 0;
}
// ---------------------------------------------------------------------------
// Game state initialisation
// ---------------------------------------------------------------------------
export function createInitialState(): GameState {
const deck = shuffle(buildDeck());
const players: [Player, Player, Player, Player] = [
{ index: 0, hand: [], pile: [], scope: 0, isHuman: true, name: 'Tu' },
{ index: 1, hand: [], pile: [], scope: 0, isHuman: false, name: 'AI Ovest' },
{ index: 2, hand: [], pile: [], scope: 0, isHuman: false, name: 'Compagno' },
{ index: 3, hand: [], pile: [], scope: 0, isHuman: false, name: 'AI Est' },
];
// Deal 10 cards each — Scopone Scientifico deals all at once
for (let i = 0; i < 4; i++) {
players[i].hand = deck.splice(0, 10);
}
// No initial table cards in Scopone Scientifico
const table: Card[] = [];
const emptyTeamScore = (): TeamScore => ({
cards: 0, scope: 0, denari: 0, settebello: false, primiera: 0, roundPoints: 0, totalPoints: 0,
});
return {
players,
table,
currentPlayer: 0,
roundOver: false,
gameOver: false,
teamScores: [emptyTeamScore(), emptyTeamScore()],
lastCapturTeam: null,
roundNumber: 1,
};
}
// ---------------------------------------------------------------------------
// Play a card
// ---------------------------------------------------------------------------
/**
* Apply a move to the game state (immutably).
* If captureChoice is provided, use that capture set; otherwise use the first valid capture.
* Returns the new state and what was captured (null if nothing).
*/
export function applyMove(
state: GameState,
playerIdx: PlayerIndex,
card: Card,
captureChoice?: Card[]
): { nextState: GameState; capture: Capture | null; isScopa: boolean } {
const state2 = deepClone(state);
const player = state2.players[playerIdx];
// Remove card from hand
player.hand = player.hand.filter(c => c.id !== card.id);
const captures = findCaptures(card, state2.table);
let capturedCards: Card[] = [];
let isScopa = false;
if (captures.length > 0) {
const chosen = captureChoice ?? captures[0];
capturedCards = chosen;
// Remove captured cards from table
const capturedIds = new Set(chosen.map(c => c.id));
state2.table = state2.table.filter(c => !capturedIds.has(c.id));
// Add played card + captured to player's pile
player.pile.push(card, ...capturedCards);
// Scopa: cleared the table
if (state2.table.length === 0) {
player.scope += 1;
isScopa = true;
}
// Track which team made last capture
state2.lastCapturTeam = (playerIdx === 0 || playerIdx === 2) ? 0 : 1;
} else {
// No capture — add to table
state2.table.push(card);
}
// Advance turn
state2.currentPlayer = ((playerIdx + 1) % 4) as PlayerIndex;
// Check if round is over (all hands empty)
const allHandsEmpty = state2.players.every(p => p.hand.length === 0);
if (allHandsEmpty) {
// Remaining table cards go to last capturing team
if (state2.table.length > 0 && state2.lastCapturTeam !== null) {
const lastTeamPlayers = state2.lastCapturTeam === 0 ? [0, 2] : [1, 3];
// Give to first player of that team
state2.players[lastTeamPlayers[0]].pile.push(...state2.table);
state2.table = [];
}
state2.roundOver = true;
calculateScores(state2);
}
return {
nextState: state2,
capture: capturedCards.length > 0 ? { played: card, captured: capturedCards } : null,
isScopa,
};
}
// ---------------------------------------------------------------------------
// Scoring
// ---------------------------------------------------------------------------
function calculateScores(state: GameState): void {
const team0 = [state.players[0], state.players[2]];
const team1 = [state.players[1], state.players[3]];
const breakdown: ScoreBreakdown = scoreRound(team0, team1);
const t0 = state.teamScores[0];
const t1 = state.teamScores[1];
// Cards
t0.cards = team0.reduce((s, p) => s + p.pile.length, 0);
t1.cards = team1.reduce((s, p) => s + p.pile.length, 0);
// Denari
const denari0 = team0.flatMap(p => p.pile).filter(c => c.suit === 'denara');
const denari1 = team1.flatMap(p => p.pile).filter(c => c.suit === 'denara');
t0.denari = denari0.length;
t1.denari = denari1.length;
// Settebello
t0.settebello = team0.flatMap(p => p.pile).some(c => c.suit === 'denara' && c.value === 7);
t1.settebello = !t0.settebello;
// Scope
t0.scope = team0.reduce((s, p) => s + p.scope, 0);
t1.scope = team1.reduce((s, p) => s + p.scope, 0);
// Primiera
t0.primiera = calcPrimiera(team0.flatMap(p => p.pile));
t1.primiera = calcPrimiera(team1.flatMap(p => p.pile));
// Points this round
let p0 = 0;
let p1 = 0;
if (breakdown.cartePoint === 0) p0++;
else if (breakdown.cartePoint === 1) p1++;
if (breakdown.denariPoint === 0) p0++;
else if (breakdown.denariPoint === 1) p1++;
if (breakdown.settebelloPoint === 0) p0++;
else p1++;
if (breakdown.primieraPoint === 0) p0++;
else if (breakdown.primieraPoint === 1) p1++;
p0 += breakdown.scopeTeam0;
p1 += breakdown.scopeTeam1;
t0.roundPoints = p0;
t1.roundPoints = p1;
t0.totalPoints += p0;
t1.totalPoints += p1;
}
function scoreRound(team0: Player[], team1: Player[]): ScoreBreakdown {
const pile0 = team0.flatMap(p => p.pile);
const pile1 = team1.flatMap(p => p.pile);
const cards0 = pile0.length;
const cards1 = pile1.length;
const denari0 = pile0.filter(c => c.suit === 'denara').length;
const denari1 = pile1.filter(c => c.suit === 'denara').length;
const hasSette0 = pile0.some(c => c.suit === 'denara' && c.value === 7);
const prim0 = calcPrimiera(pile0);
const prim1 = calcPrimiera(pile1);
const scope0 = team0.reduce((s, p) => s + p.scope, 0);
const scope1 = team1.reduce((s, p) => s + p.scope, 0);
return {
cartePoint: cards0 > cards1 ? 0 : cards1 > cards0 ? 1 : null,
denariPoint: denari0 > denari1 ? 0 : denari1 > denari0 ? 1 : null,
settebelloPoint: hasSette0 ? 0 : 1,
primieraPoint: prim0 > prim1 ? 0 : prim1 > prim0 ? 1 : null,
scopeTeam0: scope0,
scopeTeam1: scope1,
};
}
export function calcPrimiera(pile: Card[]): number {
// Best card per suit, using primiera values
let total = 0;
for (const suit of SUITS) {
const cards = pile.filter(c => c.suit === suit);
if (cards.length === 0) return 0; // can't score primiera without all 4 suits
const best = Math.max(...cards.map(c => PRIMIERA_VALUES[c.value]));
total += best;
}
return total;
}
export function getScoreBreakdown(state: GameState): ScoreBreakdown {
const team0 = [state.players[0], state.players[2]];
const team1 = [state.players[1], state.players[3]];
return scoreRound(team0, team1);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
export function teamOf(playerIdx: PlayerIndex): 0 | 1 {
return (playerIdx === 0 || playerIdx === 2) ? 0 : 1;
}

69
src/game/types.ts Normal file
View File

@@ -0,0 +1,69 @@
export type Suit = 'bastoni' | 'coppe' | 'denara' | 'spade';
export const SUITS: Suit[] = ['bastoni', 'coppe', 'denara', 'spade'];
export interface Card {
suit: Suit;
value: number; // 1-10
id: string; // e.g. "denara_7"
}
export interface Capture {
played: Card;
captured: Card[];
}
export type PlayerIndex = 0 | 1 | 2 | 3;
export interface Player {
index: PlayerIndex;
hand: Card[];
pile: Card[]; // captured cards
scope: number; // number of scope achieved
isHuman: boolean;
name: string;
}
export interface GameState {
players: [Player, Player, Player, Player];
table: Card[];
currentPlayer: PlayerIndex;
roundOver: boolean;
gameOver: boolean;
teamScores: [TeamScore, TeamScore]; // Team 0: players 0+2, Team 1: players 1+3
lastCapturTeam: 0 | 1 | null; // which team made the last capture (gets remaining table cards)
roundNumber: number;
}
export interface TeamScore {
cards: number;
scope: number;
denari: number;
settebello: boolean;
primiera: number;
// final points for this round
roundPoints: number;
totalPoints: number;
}
export interface ScoreBreakdown {
cartePoint: 0 | 1 | null; // null = tie
denariPoint: 0 | 1 | null;
settebelloPoint: 0 | 1;
primieraPoint: 0 | 1 | null;
scopeTeam0: number;
scopeTeam1: number;
}
// Primiera values per card value
export const PRIMIERA_VALUES: Record<number, number> = {
7: 21,
6: 18,
1: 16,
5: 15,
4: 14,
3: 13,
2: 12,
8: 10,
9: 10,
10: 10,
};

19
src/main.ts Normal file
View File

@@ -0,0 +1,19 @@
import Phaser from 'phaser';
import { BootScene } from './scenes/BootScene';
import { MenuScene } from './scenes/MenuScene';
import { GameScene } from './scenes/GameScene';
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 1280,
height: 720,
backgroundColor: '#1a5c2a',
parent: 'game',
scene: [BootScene, MenuScene, GameScene],
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
};
new Phaser.Game(config);

47
src/scenes/BootScene.ts Normal file
View File

@@ -0,0 +1,47 @@
import Phaser from 'phaser';
/**
* BootScene — loads all assets before the game starts.
* Uses atlas loaded from public/atlas.json (Phaser hash format, converted from Napoletane atlas).
*
* [trueref /local/phaser] — this.load.atlas() loads a texture atlas where frames are
* referenced by name (e.g. 'bastoni_7') using the Phaser hash format JSON.
*/
export class BootScene extends Phaser.Scene {
constructor() {
super({ key: 'BootScene' });
}
preload(): void {
const W = this.scale.width;
const H = this.scale.height;
// Loading bar
const bar = this.add.rectangle(W / 2, H / 2, 400, 20, 0x4caf50);
const barBg = this.add.rectangle(W / 2, H / 2, 402, 22, 0x1a5c2a);
barBg.setDepth(0);
bar.setDepth(1);
bar.setOrigin(0.5);
bar.scaleX = 0;
const label = this.add.text(W / 2, H / 2 - 40, 'Caricamento...', {
fontFamily: 'serif',
fontSize: '24px',
color: '#ffffff',
}).setOrigin(0.5);
this.load.on('progress', (value: number) => {
bar.scaleX = value;
});
// Load card atlas (Phaser hash format converted from Napoletane atlas)
this.load.atlas('cards', 'atlas.png', 'atlas.json');
// Load card back separately
this.load.image('retro', 'retro.png');
}
create(): void {
this.scene.start('MenuScene');
}
}

1331
src/scenes/GameScene.ts Normal file

File diff suppressed because it is too large Load Diff

71
src/scenes/MenuScene.ts Normal file
View File

@@ -0,0 +1,71 @@
import Phaser from 'phaser';
export class MenuScene extends Phaser.Scene {
constructor() {
super({ key: 'MenuScene' });
}
create(): void {
const W = this.scale.width;
const H = this.scale.height;
// Background felt
this.add.rectangle(0, 0, W, H, 0x1a5c2a).setOrigin(0);
// Title
this.add.text(W / 2, H * 0.2, 'Scopone Scientifico', {
fontFamily: 'Georgia, serif',
fontSize: '52px',
color: '#ffd700',
stroke: '#000000',
strokeThickness: 4,
}).setOrigin(0.5);
this.add.text(W / 2, H * 0.32, '2 vs 2 · Tu + Compagno vs 2 AI', {
fontFamily: 'serif',
fontSize: '22px',
color: '#ccffcc',
}).setOrigin(0.5);
// Rules summary
const rules = [
'40 carte Napoletane · 10 a testa',
'Cattura per valore o somma',
'Punteggio: Carte · Denari · Settebello · Primiera · Scope',
'Prima squadra a 11 punti vince',
];
rules.forEach((line, i) => {
this.add.text(W / 2, H * 0.44 + i * 28, line, {
fontFamily: 'serif',
fontSize: '18px',
color: '#ffffff',
}).setOrigin(0.5);
});
// Start button
const btn = this.add.rectangle(W / 2, H * 0.72, 220, 60, 0xffd700, 1)
.setInteractive({ useHandCursor: true });
const btnText = this.add.text(W / 2, H * 0.72, 'INIZIA PARTITA', {
fontFamily: 'Georgia, serif',
fontSize: '22px',
color: '#1a5c2a',
}).setOrigin(0.5);
btn.on('pointerover', () => btn.setFillStyle(0xffec6e));
btn.on('pointerout', () => btn.setFillStyle(0xffd700));
btn.on('pointerdown', () => {
this.cameras.main.fadeOut(300, 0, 30, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start('GameScene');
});
});
// Show some face-down cards decoratively
const positions = [
[W * 0.1, H * 0.5], [W * 0.15, H * 0.52], [W * 0.9, H * 0.5], [W * 0.85, H * 0.52],
];
for (const [x, y] of positions) {
this.add.image(x, y, 'retro').setScale(0.08).setAngle(Phaser.Math.Between(-15, 15));
}
}
}

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true
},
"include": ["src"]
}

14
vite.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
export default defineConfig({
root: '.',
publicDir: 'public',
build: {
outDir: 'dist',
emptyOutDir: true,
},
server: {
port: 3000,
open: true,
},
});