From d3915d7e9c7bc31d109e0353c1bafe0832f86150 Mon Sep 17 00:00:00 2001 From: Boxel Submission Bot Date: Wed, 25 Mar 2026 15:58:41 +0800 Subject: [PATCH] add Risk Game changes [boxel-content-hash:e7fc0e96e676] --- .../fd10a958-f8ef-4b31-ab15-fb3ad74721de.json | 79 +++ .../1d34684d-03d7-494b-be30-1f7dffe91ac6.json | 40 ++ .../34684d03-d739-4bbe-b01f-7dffe91ac68e.json | 40 ++ .../684d03d7-394b-4e30-9f7d-ffe91ac68ef4.json | 40 ++ risk/Continent/midrealm.json | 43 ++ risk/Continent/northland.json | 20 + risk/Continent/southreach.json | 58 +++ risk/Player/azul.json | 17 + risk/Player/crimson.json | 17 + risk/Territory/m1.json | 54 ++ risk/Territory/m2.json | 15 + risk/Territory/m3.json | 54 ++ risk/Territory/n1.json | 49 ++ risk/Territory/n2.json | 54 ++ risk/Territory/n3.json | 49 ++ risk/Territory/s1.json | 54 ++ risk/Territory/s2.json | 59 +++ risk/Territory/s3.json | 54 ++ risk/Territory/w1.json | 49 ++ risk/Territory/w2.json | 54 ++ risk/Territory/w3.json | 49 ++ risk/continent.gts | 124 +++++ risk/game.gts | 478 ++++++++++++++++++ risk/game.json | 125 +++++ risk/player.gts | 141 ++++++ risk/territory.gts | 149 ++++++ 26 files changed, 1965 insertions(+) create mode 100644 AppListing/fd10a958-f8ef-4b31-ab15-fb3ad74721de.json create mode 100644 Spec/1d34684d-03d7-494b-be30-1f7dffe91ac6.json create mode 100644 Spec/34684d03-d739-4bbe-b01f-7dffe91ac68e.json create mode 100644 Spec/684d03d7-394b-4e30-9f7d-ffe91ac68ef4.json create mode 100644 risk/Continent/midrealm.json create mode 100644 risk/Continent/northland.json create mode 100644 risk/Continent/southreach.json create mode 100644 risk/Player/azul.json create mode 100644 risk/Player/crimson.json create mode 100644 risk/Territory/m1.json create mode 100644 risk/Territory/m2.json create mode 100644 risk/Territory/m3.json create mode 100644 risk/Territory/n1.json create mode 100644 risk/Territory/n2.json create mode 100644 risk/Territory/n3.json create mode 100644 risk/Territory/s1.json create mode 100644 risk/Territory/s2.json create mode 100644 risk/Territory/s3.json create mode 100644 risk/Territory/w1.json create mode 100644 risk/Territory/w2.json create mode 100644 risk/Territory/w3.json create mode 100644 risk/continent.gts create mode 100644 risk/game.gts create mode 100644 risk/game.json create mode 100644 risk/player.gts create mode 100644 risk/territory.gts diff --git a/AppListing/fd10a958-f8ef-4b31-ab15-fb3ad74721de.json b/AppListing/fd10a958-f8ef-4b31-ab15-fb3ad74721de.json new file mode 100644 index 0000000..cd24857 --- /dev/null +++ b/AppListing/fd10a958-f8ef-4b31-ab15-fb3ad74721de.json @@ -0,0 +1,79 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "AppListing", + "module": "https://realms-staging.stack.cards/catalog/catalog-app/listing/listing" + } + }, + "type": "card", + "attributes": { + "name": "Risk Game", + "images": [], + "summary": "The Game class defines the state and interface for a Risk game implementation, encompassing game setup, player turns, and map interactions. It manages participants, territories, continents, current turn, game phases (placement, attack, fortify), and dice rolls. The class provides mechanisms for placing armies, attacking territories with dice rolls, resolving battles, and ending turns, including reinforcement calculations based on territory ownership and continent control. The associated user interface offers game controls, territory selection, and visual indicators of game status, supporting interactive gameplay.", + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "skills": { + "links": { + "self": null + } + }, + "tags.0": { + "links": { + "self": "https://realms-staging.stack.cards/catalog/Tag/140feda8-625b-4a24-9ddb-6f4da891aef2" + } + }, + "tags.1": { + "links": { + "self": "https://realms-staging.stack.cards/catalog/Tag/4d0f9ae2-048e-4ce0-b263-7006602ce6a4" + } + }, + "license": { + "links": { + "self": "https://realms-staging.stack.cards/catalog/License/4c5a023b-a72c-4f90-930b-da60a1de5b2d" + } + }, + "specs.0": { + "links": { + "self": "../Spec/684d03d7-394b-4e30-9f7d-ffe91ac68ef4" + } + }, + "specs.1": { + "links": { + "self": "../Spec/34684d03-d739-4bbe-b01f-7dffe91ac68e" + } + }, + "specs.2": { + "links": { + "self": "../Spec/1d34684d-03d7-494b-be30-1f7dffe91ac6" + } + }, + "publisher": { + "links": { + "self": null + } + }, + "categories": { + "links": { + "self": null + } + }, + "examples.0": { + "links": { + "self": "../risk/game" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/Spec/1d34684d-03d7-494b-be30-1f7dffe91ac6.json b/Spec/1d34684d-03d7-494b-be30-1f7dffe91ac6.json new file mode 100644 index 0000000..ccc5bea --- /dev/null +++ b/Spec/1d34684d-03d7-494b-be30-1f7dffe91ac6.json @@ -0,0 +1,40 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../risk/territory", + "name": "Territory" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "Territory", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} \ No newline at end of file diff --git a/Spec/34684d03-d739-4bbe-b01f-7dffe91ac68e.json b/Spec/34684d03-d739-4bbe-b01f-7dffe91ac68e.json new file mode 100644 index 0000000..b31ca63 --- /dev/null +++ b/Spec/34684d03-d739-4bbe-b01f-7dffe91ac68e.json @@ -0,0 +1,40 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../risk/player", + "name": "Player" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "Player", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} \ No newline at end of file diff --git a/Spec/684d03d7-394b-4e30-9f7d-ffe91ac68ef4.json b/Spec/684d03d7-394b-4e30-9f7d-ffe91ac68ef4.json new file mode 100644 index 0000000..430164a --- /dev/null +++ b/Spec/684d03d7-394b-4e30-9f7d-ffe91ac68ef4.json @@ -0,0 +1,40 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../risk/continent", + "name": "Continent" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "Continent", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} \ No newline at end of file diff --git a/risk/Continent/midrealm.json b/risk/Continent/midrealm.json new file mode 100644 index 0000000..77119aa --- /dev/null +++ b/risk/Continent/midrealm.json @@ -0,0 +1,43 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Continent", + "module": "../continent" + } + }, + "type": "card", + "attributes": { + "name": "Midrealm", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "bonusArmies": 3 + }, + "relationships": { + "territories.0": { + "links": { + "self": "../Territory/m1" + } + }, + "territories.1": { + "links": { + "self": "../Territory/m2" + } + }, + "territories.2": { + "links": { + "self": "../Territory/m3" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Continent/northland.json b/risk/Continent/northland.json new file mode 100644 index 0000000..396a8b8 --- /dev/null +++ b/risk/Continent/northland.json @@ -0,0 +1,20 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "Northland", + "bonusArmies": 3 + }, + "relationships": { + "territories.0": { "links": { "self": "../Territory/n1" } }, + "territories.1": { "links": { "self": "../Territory/n2" } }, + "territories.2": { "links": { "self": "../Territory/n3" } } + }, + "meta": { + "adoptsFrom": { + "name": "Continent", + "module": "../continent" + } + } + } +} \ No newline at end of file diff --git a/risk/Continent/southreach.json b/risk/Continent/southreach.json new file mode 100644 index 0000000..0ecd02a --- /dev/null +++ b/risk/Continent/southreach.json @@ -0,0 +1,58 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Continent", + "module": "../continent" + } + }, + "type": "card", + "attributes": { + "name": "Southreach", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "bonusArmies": 4 + }, + "relationships": { + "territories.0": { + "links": { + "self": "../Territory/s1" + } + }, + "territories.1": { + "links": { + "self": "../Territory/s2" + } + }, + "territories.2": { + "links": { + "self": "../Territory/s3" + } + }, + "territories.3": { + "links": { + "self": "../Territory/w1" + } + }, + "territories.4": { + "links": { + "self": "../Territory/w2" + } + }, + "territories.5": { + "links": { + "self": "../Territory/w3" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Player/azul.json b/risk/Player/azul.json new file mode 100644 index 0000000..fb9b41d --- /dev/null +++ b/risk/Player/azul.json @@ -0,0 +1,17 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "Azul", + "color": "#2563eb", + "reserves": 10, + "eliminated": "" + }, + "meta": { + "adoptsFrom": { + "name": "Player", + "module": "../player" + } + } + } +} \ No newline at end of file diff --git a/risk/Player/crimson.json b/risk/Player/crimson.json new file mode 100644 index 0000000..c45a55d --- /dev/null +++ b/risk/Player/crimson.json @@ -0,0 +1,17 @@ +{ + "data": { + "type": "card", + "attributes": { + "name": "Crimson", + "color": "#dc2626", + "reserves": 10, + "eliminated": "" + }, + "meta": { + "adoptsFrom": { + "name": "Player", + "module": "../player" + } + } + } +} \ No newline at end of file diff --git a/risk/Territory/m1.json b/risk/Territory/m1.json new file mode 100644 index 0000000..68db303 --- /dev/null +++ b/risk/Territory/m1.json @@ -0,0 +1,54 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Rivergate", + "armies": 1, + "shortId": "M1", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/crimson" + } + }, + "continent": { + "links": { + "self": "../Continent/midrealm" + } + }, + "neighbors.0": { + "links": { + "self": "./n1" + } + }, + "neighbors.1": { + "links": { + "self": "./m2" + } + }, + "neighbors.2": { + "links": { + "self": "./s1" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/m2.json b/risk/Territory/m2.json new file mode 100644 index 0000000..e0f4184 --- /dev/null +++ b/risk/Territory/m2.json @@ -0,0 +1,15 @@ +{ + "data": { + "type": "card", + "attributes": { "name": "Ironfields", "shortId": "M2", "armies": 1 }, + "relationships": { + "owner": { "links": { "self": "../Player/azul" } }, + "continent": { "links": { "self": "../Continent/midrealm" } }, + "neighbors.0": { "links": { "self": "../Territory/n2" } }, + "neighbors.1": { "links": { "self": "../Territory/m1" } }, + "neighbors.2": { "links": { "self": "./m3" } }, + "neighbors.3": { "links": { "self": "../Territory/s2" } } + }, + "meta": { "adoptsFrom": { "module": "../territory", "name": "Territory" } } + } +} \ No newline at end of file diff --git a/risk/Territory/m3.json b/risk/Territory/m3.json new file mode 100644 index 0000000..6c5ce5f --- /dev/null +++ b/risk/Territory/m3.json @@ -0,0 +1,54 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Highcrest", + "armies": 1, + "shortId": "M3", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/crimson" + } + }, + "continent": { + "links": { + "self": "../Continent/midrealm" + } + }, + "neighbors.0": { + "links": { + "self": "./n3" + } + }, + "neighbors.1": { + "links": { + "self": "./m2" + } + }, + "neighbors.2": { + "links": { + "self": "./s3" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/n1.json b/risk/Territory/n1.json new file mode 100644 index 0000000..1506f6c --- /dev/null +++ b/risk/Territory/n1.json @@ -0,0 +1,49 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Glacier Bay", + "armies": 1, + "shortId": "N1", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/azul" + } + }, + "continent": { + "links": { + "self": "../Continent/northland" + } + }, + "neighbors.0": { + "links": { + "self": "./n2" + } + }, + "neighbors.1": { + "links": { + "self": "./m1" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/n2.json b/risk/Territory/n2.json new file mode 100644 index 0000000..3f6881a --- /dev/null +++ b/risk/Territory/n2.json @@ -0,0 +1,54 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Frostvale", + "armies": 1, + "shortId": "N2", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/crimson" + } + }, + "continent": { + "links": { + "self": "../Continent/northland" + } + }, + "neighbors.0": { + "links": { + "self": "./n1" + } + }, + "neighbors.1": { + "links": { + "self": "./n3" + } + }, + "neighbors.2": { + "links": { + "self": "./m2" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/n3.json b/risk/Territory/n3.json new file mode 100644 index 0000000..e0b5df0 --- /dev/null +++ b/risk/Territory/n3.json @@ -0,0 +1,49 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Aurora Ridge", + "armies": 1, + "shortId": "N3", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/azul" + } + }, + "continent": { + "links": { + "self": "../Continent/northland" + } + }, + "neighbors.0": { + "links": { + "self": "./n2" + } + }, + "neighbors.1": { + "links": { + "self": "./m3" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/s1.json b/risk/Territory/s1.json new file mode 100644 index 0000000..371ef0a --- /dev/null +++ b/risk/Territory/s1.json @@ -0,0 +1,54 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Suncove", + "armies": 1, + "shortId": "S1", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/azul" + } + }, + "continent": { + "links": { + "self": "../Continent/southreach" + } + }, + "neighbors.0": { + "links": { + "self": "./m1" + } + }, + "neighbors.1": { + "links": { + "self": "./s2" + } + }, + "neighbors.2": { + "links": { + "self": "./w1" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/s2.json b/risk/Territory/s2.json new file mode 100644 index 0000000..9f8d450 --- /dev/null +++ b/risk/Territory/s2.json @@ -0,0 +1,59 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Redtide", + "armies": 1, + "shortId": "S2", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/crimson" + } + }, + "continent": { + "links": { + "self": "../Continent/southreach" + } + }, + "neighbors.0": { + "links": { + "self": "./m2" + } + }, + "neighbors.1": { + "links": { + "self": "./s1" + } + }, + "neighbors.2": { + "links": { + "self": "./s3" + } + }, + "neighbors.3": { + "links": { + "self": "./w2" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/s3.json b/risk/Territory/s3.json new file mode 100644 index 0000000..b857918 --- /dev/null +++ b/risk/Territory/s3.json @@ -0,0 +1,54 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Mesa Verde", + "armies": 1, + "shortId": "S3", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/azul" + } + }, + "continent": { + "links": { + "self": "../Continent/southreach" + } + }, + "neighbors.0": { + "links": { + "self": "./m3" + } + }, + "neighbors.1": { + "links": { + "self": "./s2" + } + }, + "neighbors.2": { + "links": { + "self": "./w3" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/w1.json b/risk/Territory/w1.json new file mode 100644 index 0000000..3b57c10 --- /dev/null +++ b/risk/Territory/w1.json @@ -0,0 +1,49 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Windsteppe", + "armies": 1, + "shortId": "W1", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/crimson" + } + }, + "continent": { + "links": { + "self": "../Continent/southreach" + } + }, + "neighbors.0": { + "links": { + "self": "./s1" + } + }, + "neighbors.1": { + "links": { + "self": "./w2" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/w2.json b/risk/Territory/w2.json new file mode 100644 index 0000000..42c6db8 --- /dev/null +++ b/risk/Territory/w2.json @@ -0,0 +1,54 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Stonepass", + "armies": 1, + "shortId": "W2", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/azul" + } + }, + "continent": { + "links": { + "self": "../Continent/southreach" + } + }, + "neighbors.0": { + "links": { + "self": "./s2" + } + }, + "neighbors.1": { + "links": { + "self": "./w1" + } + }, + "neighbors.2": { + "links": { + "self": "./w3" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/Territory/w3.json b/risk/Territory/w3.json new file mode 100644 index 0000000..93434f2 --- /dev/null +++ b/risk/Territory/w3.json @@ -0,0 +1,49 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Territory", + "module": "../territory" + } + }, + "type": "card", + "attributes": { + "name": "Goldbar", + "armies": 1, + "shortId": "W3", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "owner": { + "links": { + "self": "../Player/crimson" + } + }, + "continent": { + "links": { + "self": "../Continent/southreach" + } + }, + "neighbors.0": { + "links": { + "self": "./s3" + } + }, + "neighbors.1": { + "links": { + "self": "./w2" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/risk/continent.gts b/risk/continent.gts new file mode 100644 index 0000000..2e43b44 --- /dev/null +++ b/risk/continent.gts @@ -0,0 +1,124 @@ +import { gt } from '@cardstack/boxel-ui/helpers'; +/* + Risk: Continent definition + - CardDef because continents are referenced by territories and game state. +*/ +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { + CardDef, + FieldDef, + Component, + field, + contains, + containsMany, + linksToMany, +} from 'https://cardstack.com/base/card-api'; // ¹ Core +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import MapPinIcon from '@cardstack/boxel-icons/map-pin'; // ² icon +import { Territory } from './territory'; // ³ bring symbol into scope for thunk + +export class Continent extends CardDef { + // ³ Card definition + static displayName = 'Continent'; + static icon = MapPinIcon; + + @field name = contains(StringField); // ⁴ Name + @field bonusArmies = contains(NumberField); // ⁵ Bonus per Risk rules + @field territories = linksToMany(() => Territory); // ⁶ Territories in this continent (set by game/map) + + // ⁷ Compute inherited title + @field cardTitle = contains(StringField, { + computeVia: function (this: Continent) { + try { + const n = this.name ?? 'Unnamed Continent'; + const b = this.bonusArmies != null ? ` (+${this.bonusArmies})` : ''; + return `${n}${b}`; + } catch { + return 'Continent'; + } + }, + }); + + static embedded = class Embedded extends Component { + + }; + + static fitted = class Fitted extends Component { + + }; + + // Additional formats or components +} diff --git a/risk/game.gts b/risk/game.gts new file mode 100644 index 0000000..e2471af --- /dev/null +++ b/risk/game.gts @@ -0,0 +1,478 @@ +import { concat, fn } from '@ember/helper'; +/* + Risk: Game definition + - Holds full game state and interactive UI for placement, attack, fortify, end turn. + - Mini-map with ~12 territories across 3 continents. +*/ +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { + CardDef, + FieldDef, + Component, + field, + contains, + containsMany, + linksTo, + linksToMany, +} from 'https://cardstack.com/base/card-api'; // ¹ +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import BooleanField from 'https://cardstack.com/base/boolean'; +import { tracked } from '@glimmer/tracking'; +import { Button } from '@cardstack/boxel-ui/components'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import DiceIcon from '@cardstack/boxel-icons/dice-5'; // ² +import { Player } from './player'; +import { Territory } from './territory'; +import { Continent } from './continent'; +import { formatNumber, eq, gt, or } from '@cardstack/boxel-ui/helpers'; + +export class Game extends CardDef { + // ³ + static displayName = 'Risk Game'; + static icon = DiceIcon; + static prefersWideFormat = true; + + // Core state + @field name = contains(StringField); // ⁴ game name + @field players = linksToMany(Player); // ⁵ participants + @field territories = linksToMany(Territory); // ⁶ map territories + @field continents = linksToMany(Continent); // ⁷ map continents + @field currentPlayerIndex = contains(NumberField); // ⁸ whose turn (0-based) + @field phase = contains(StringField); // ⁹ 'placement' | 'attack' | 'fortify' + @field pendingFrom = linksTo(Territory); // ¹⁰ selection for attack/fortify + @field pendingTo = linksTo(Territory); // ¹¹ selection to target + @field lastRoll = contains(StringField); // ¹² dice result text + + // Display title + @field cardTitle = contains(StringField, { + computeVia: function (this: Game) { + try { + const n = this.name ?? 'Risk Game'; + return n; + } catch { + return 'Risk Game'; + } + }, + }); + + static isolated = class Isolated extends Component { + @tracked selectedFrom?: Territory; + @tracked selectedTo?: Territory; + + get playerCount() { + return this.args?.model?.players?.length ?? 0; + } + + get currentPlayer(): Player | undefined { + try { + const idx = this.args?.model?.currentPlayerIndex ?? 0; + return this.args?.model?.players?.[idx]; + } catch { + return undefined; + } + } + + get canAttack() { + return (this.args?.model?.phase ?? 'placement') === 'attack'; + } + get canFortify() { + return (this.args?.model?.phase ?? 'placement') === 'fortify'; + } + + // Helpers + private rollDice = (count: number) => { + const rolls = Array.from( + { length: count }, + () => Math.floor(Math.random() * 6) + 1, + ); + return rolls.sort((a, b) => b - a); + }; + + private resolveBattle = () => { + const from = this.args.model?.pendingFrom; + const to = this.args.model?.pendingTo; + const attacker = from?.owner; + const defender = to?.owner; + + if (!from || !to || !attacker || !defender) { + this.args.model.lastRoll = 'Select valid territories.'; + return; + } + const attackDice = Math.min(3, Math.max(0, (from.armies ?? 0) - 1)); + const defendDice = Math.min(2, Math.max(1, (to.armies ?? 0) > 0 ? 1 : 0)); + + if (attackDice < 1 || defendDice < 1) { + this.args.model.lastRoll = 'Not enough armies to attack or defend.'; + return; + } + const aRolls = this.rollDice(attackDice); + const dRolls = this.rollDice(defendDice); + + let aLoss = 0; + let dLoss = 0; + const pairs = Math.min(aRolls.length, dRolls.length); + for (let i = 0; i < pairs; i++) { + if (aRolls[i] > dRolls[i]) { + dLoss++; + } else { + aLoss++; + } + } + from.armies = Math.max(0, (from.armies ?? 0) - aLoss); + to.armies = Math.max(0, (to.armies ?? 0) - dLoss); + + // Capture if defender reduced to 0 + if ((to.armies ?? 0) === 0 && (from.armies ?? 0) > 1) { + to.owner = attacker; + const moveIn = Math.min((from.armies ?? 2) - 1, attackDice); // move at least 1 up to dice count + from.armies = (from.armies ?? 0) - moveIn; + to.armies = moveIn; + this.args.model.lastRoll = `A:${aRolls.join(',')} vs D:${dRolls.join( + ',', + )} → Captured! (moved ${moveIn})`; + } else { + this.args.model.lastRoll = `A:${aRolls.join(',')} vs D:${dRolls.join( + ',', + )} → Loss A:${aLoss} D:${dLoss}`; + } + }; + + private endTurn = () => { + const players = this.args.model?.players ?? []; + if (!players.length) return; + const next = + ((this.args.model.currentPlayerIndex ?? 0) + 1) % players.length; + this.args.model.currentPlayerIndex = next; + this.args.model.phase = 'placement'; + this.args.model.pendingFrom = undefined as unknown as Territory; + this.args.model.pendingTo = undefined as unknown as Territory; + this.args.model.lastRoll = ''; + // Basic reinforcements: max(3, floor(owned/3)) + continent bonuses + const me = players[next]; + const owned = (this.args.model.territories ?? []).filter( + (t) => t?.owner?.id === me?.id, + ).length; + let reinf = Math.max(3, Math.floor((owned || 0) / 3)); + // Continent bonus: if player owns all territories in continent + for (const c of this.args.model.continents ?? []) { + const allOwned = (c?.territories ?? []).every( + (tt: Territory) => tt?.owner?.id === me?.id, + ); + if (allOwned) reinf += c?.bonusArmies ?? 0; + } + me.reserves = (me.reserves ?? 0) + reinf; + }; + + private placeArmy = (territory: Territory) => { + const p = this.currentPlayer; + if (!p) return; + if ((p.reserves ?? 0) <= 0) return; + if (territory?.owner?.id !== p.id && territory?.owner != null) return; // only your territories or unowned during initial + territory.owner = p; + territory.armies = (territory.armies ?? 0) + 1; + p.reserves = (p.reserves ?? 0) - 1; + }; + + private stepPhase = () => { + const phase = this.args.model?.phase ?? 'placement'; + if (phase === 'placement') this.args.model.phase = 'attack'; + else if (phase === 'attack') this.args.model.phase = 'fortify'; + else this.endTurn(); + }; + + // Template actions + selectFrom = (t: Territory) => { + this.args.model.pendingFrom = t; + }; + selectTo = (t: Territory) => { + this.args.model.pendingTo = t; + }; + attack = () => { + if (this.canAttack) this.resolveBattle(); + }; + fortify = () => { + const from = this.args.model?.pendingFrom; + const to = this.args.model?.pendingTo; + const me = this.currentPlayer; + if ( + !from || + !to || + !me || + from.owner?.id !== me.id || + to.owner?.id !== me.id + ) { + this.args.model.lastRoll = 'Select your two territories.'; + return; + } + if ((from.armies ?? 0) <= 1) { + this.args.model.lastRoll = 'Need >1 army to move.'; + return; + } + const move = Math.max(1, Math.floor(((from.armies ?? 0) - 1) / 2)); + from.armies = (from.armies ?? 0) - move; + to.armies = (to.armies ?? 0) + move; + this.args.model.lastRoll = `Fortified: moved ${move}`; + }; + + placeOn = (t: Territory) => { + this.placeArmy(t); + }; + + + }; + + static embedded = class Embedded extends Component { + + }; + + static fitted = class Fitted extends Component { + + }; + + // Additional formats or components +} diff --git a/risk/game.json b/risk/game.json new file mode 100644 index 0000000..64e857b --- /dev/null +++ b/risk/game.json @@ -0,0 +1,125 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Game", + "module": "./game" + } + }, + "type": "card", + "attributes": { + "name": "Mini Risk", + "phase": "placement", + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + }, + "lastRoll": "", + "currentPlayerIndex": 0 + }, + "relationships": { + "pendingTo": { + "links": { + "self": "./Territory/n2" + } + }, + "players.0": { + "links": { + "self": "./Player/azul" + } + }, + "players.1": { + "links": { + "self": "./Player/crimson" + } + }, + "pendingFrom": { + "links": { + "self": "./Territory/n2" + } + }, + "continents.0": { + "links": { + "self": "./Continent/northland" + } + }, + "continents.1": { + "links": { + "self": "./Continent/midrealm" + } + }, + "continents.2": { + "links": { + "self": "./Continent/southreach" + } + }, + "territories.0": { + "links": { + "self": "./Territory/n1" + } + }, + "territories.1": { + "links": { + "self": "./Territory/n2" + } + }, + "territories.2": { + "links": { + "self": "./Territory/n3" + } + }, + "territories.3": { + "links": { + "self": "./Territory/m1" + } + }, + "territories.4": { + "links": { + "self": "./Territory/m2" + } + }, + "territories.5": { + "links": { + "self": "./Territory/m3" + } + }, + "territories.6": { + "links": { + "self": "./Territory/s1" + } + }, + "territories.7": { + "links": { + "self": "./Territory/s2" + } + }, + "territories.8": { + "links": { + "self": "./Territory/s3" + } + }, + "territories.9": { + "links": { + "self": "./Territory/w1" + } + }, + "territories.10": { + "links": { + "self": "./Territory/w2" + } + }, + "territories.11": { + "links": { + "self": "./Territory/w3" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/risk/player.gts b/risk/player.gts new file mode 100644 index 0000000..9b6f1e1 --- /dev/null +++ b/risk/player.gts @@ -0,0 +1,141 @@ +import { concat } from '@ember/helper'; +/* + Risk: Player definition +*/ +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { + CardDef, + FieldDef, + Component, + field, + contains, + containsMany, + linksToMany, +} from 'https://cardstack.com/base/card-api'; // ¹ +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import ColorField from 'https://cardstack.com/base/color'; +import UserIcon from '@cardstack/boxel-icons/user'; // ² + +export class Player extends CardDef { + // ³ + static displayName = 'Player'; + static icon = UserIcon; + + @field name = contains(StringField); // ⁴ + @field color = contains(ColorField); // ⁵ hex color + @field reserves = contains(NumberField); // ⁶ unplaced armies + @field eliminated = contains(StringField); // ⁷ status text/fallback + + // Title derived from name + @field cardTitle = contains(StringField, { + computeVia: function (this: Player) { + return this.name ?? 'Player'; + }, + }); + + static embedded = class Embedded extends Component { + + }; + + static fitted = class Fitted extends Component { + + }; + + // Additional formats or components +} diff --git a/risk/territory.gts b/risk/territory.gts new file mode 100644 index 0000000..5c526d6 --- /dev/null +++ b/risk/territory.gts @@ -0,0 +1,149 @@ +import { or } from '@cardstack/boxel-ui/helpers'; +import { concat } from '@ember/helper'; +/* + Risk: Territory definition +*/ +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { + CardDef, + FieldDef, + Component, + field, + contains, + containsMany, + linksTo, + linksToMany, +} from 'https://cardstack.com/base/card-api'; // ¹ +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import MapIcon from '@cardstack/boxel-icons/map'; // ² +import { Player } from './player'; +import { Continent } from './continent'; + +export class Territory extends CardDef { + // ³ + static displayName = 'Territory'; + static icon = MapIcon; + + @field name = contains(StringField); // ⁴ + @field shortId = contains(StringField); // ⁵ e.g., "NA-1" + @field armies = contains(NumberField); // ⁶ stationed armies + @field owner = linksTo(Player); // ⁷ owner + @field continent = linksTo(() => Continent); // ⁸ belonging continent (thunk to avoid cycle) + @field neighbors = linksToMany(() => Territory); // ⁹ adjacency + + // Title + @field cardTitle = contains(StringField, { + computeVia: function (this: Territory) { + try { + const n = this.name ?? 'Territory'; + const a = this.armies != null ? ` (${this.armies})` : ''; + return `${n}${a}`; + } catch { + return 'Territory'; + } + }, + }); + + static atom = class Atom extends Component { + + }; + + static embedded = class Embedded extends Component { + + }; + + static fitted = class Fitted extends Component { + + }; + + // Additional formats or components +}