master
bel 2023-09-16 22:30:34 -06:00
commit 6283d7f321
27 changed files with 516 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
**/*.sw*
**/__pycache__

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
- color: white
sides: [0, 0, 1, 1, 2, 2C]
- color: yellow
sides: [0, 0, 1, 2, 3, 3C]
- color: red
sides: [0, 0, 2, 3, 3, 4C]
- color: black
sides: [0, 0, 3, 3, 4, 5C]

View File

@ -0,0 +1,105 @@
- src: https://s3.amazonaws.com/geekdo-files.com/bgg338940?response-content-disposition=inline%3B%20filename%3D%22Oathsworn_player_aid_v2.pdf%22&response-content-type=application%2Fpdf&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJYFNCT7FKCE4O6TA%2F20230917%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230917T005449Z&X-Amz-SignedHeaders=host&X-Amz-Expires=120&X-Amz-Signature=c2daa6d7c83c99289b2d6698ea0ee102c84800942ebd439bd7ce948ade3595db
- stories: |
Story aid
Setup
Take permanent combat tokens. Full oathsworn health.
Use locations
Send runner - cost LVL coins, send to Banksmith or Apothecary.
Banksmith: buy LVL common cards, sell for half price (round up).
Price is card level.
Apothecary: Buy curatives (max 5) for LVL cost.
Trade coins or equip-items at any time between players and
company bag (bag limit 12 items)
Perform check/survival check: One Oathsworn. 1-10 white dice.
Tokens: Redraw (all checks), Empowered x3 (Might checks).
2 blanks is fail (do not regain tokens).
Dice value / difficulty = successes. FAIL: See story.
Round of Combat: Everyone. Might dice and 0-10 white dice.
Tokens: Redraw and Empowered 3x, no Ability Cards, Items Abilities,
or Special Abilities. 2 blanks is fail (do not regain tokens). Dice value /
defense = damage (round down). FAIL: -1 health.
- encounters: |
Encounter aid
Setup
If special rules box is checked, place it faceup. Otherwise revealed at
end of setup.
Pick 2 hands of weapons, 1 armor, 1 gear.
Set weapon might cubes, and armor/shield defense number token.
7 ability cards, choose from * to the current LVL, one with cooldown
(0), two (1), two (2), two (3).
Take max animus and place on left side, place regen tracker token if
necessary.
Pick first player.
1. Turn start
Regen aminus, pick up cards on cooldown 0. Next stage card is face
up.
2. Oathsworn take turns
Move: 1 animus per hex. All characters and terrain block movement.
Use ability card: pick card action, pay animus, resolve, place on
cooldown, battleflow.
Pass (does not end your round).
Oathsworn attack
Target closest location on large monsters.
Might value plus any number of white dice. May use combat tokens.
2+ blank is fail (dont include critical).
SUCCESS: Damage = dice roll / defense (round down).
FAIL: Regain all used combat tokens + 1 of choice.
Monster attack
Hero may play one card to increase defense, no animus cost (then
battleflow).
Roll might, no critical or miss.
Damage = dice roll / defense (round down)
Oathsworn may spend defense tokens.
3. Encounter phase
(start here if ambushed)
3a. Activate large monster(s) following stage card.
Large movement destroys terrain, will bounce on edges, moves
through characters, ending on characters pushes them (no damage).
Change facing after move.
Attacks that display icons of broken locations draw 1 less of the best
Might cards.
Face towards attack target.
3b. Minion activation order: closest target in range and LOS (ignore
terrain effects) > Target with adjacent enemy with Mob keyword (if
attacker has Mob keyword only) > NW rule. Activate minion and
move,
3c. Minions attack, mob sum up might dice to one attack.
Targeting priority
Character who broke location (reaction only) > specific target on
Stage card if in range and LOS > closest Oathsworn in range and
LOS (ignore terrain effects) > Oathsworn with adjacent enemy with
Mob keyword (if Mob keyword) > NW rule. (Never targets ally/friendly
character, except reaction)
Keywords
Area Of Effect: may affect each character only once, gain +1
damage for each extra hex of a large figure, for melee AOEs LOS
to all targets needed, for ranged AOEs only the center hex needs
to be in LOS. One dice roll for all targets. Reactions do not cancel
hits on additional targets.
Battleflow: move all cards on one cooldown position to next. No
cascade.
Caged: can't do anything. See special rules. Each adjacent friendly
character gives +2 on survival check.
Chained attack: choose target in range and LOS, add additional
targets in chain range and LOS of the previous target. See AOE.
Remove lowest (not blank) dice result, once for each additional
target.
Charge Through X: Move X in straight line, attack all enemies moved
through, see AOE.
Consumed: Stuck in body part until broken or 6 damage delt. May
attack once per round for 3 animus. See special rules on start/end
round effects.
Crippled: No move or abilities with movement, until the end of the
next Oathsworn phase.
Empowered: Upgrade dice white>yellow>red>black.
Push: move straight away, or closest NW free hex.
Knockback: push that damages 1 on collision with terrain or
character (both take damage), no damage on board edge. Large is
pushed ½ distance (round up) and destroys terrain.
Line Of Sight: between closest corners. Or if straight tile row; center
to center. Line cannot touch any terrain. Ignore characters.
Thrown: place tracker in any adjacent hex to target, cannot pick up
this round.
Thrown weapon: unarmed, might is zero. Can still attack.
Thrown armor: no defense from items, no use of thrown item.
Wave attacks: 3 hex wide line of attack, nothing blocks it.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,108 @@
from yaml import safe_load as yaml_load
from yaml import dump as yaml_dump
from sys import argv
def main():
path = "./testdata/hello_world.yaml"
if len(argv) > 1:
path = argv[1]
doc = Doc.from_file(path)
print(doc.render())
print(doc.to_dict())
# https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/
class Doc():
def __init__(self):
self._nodes = []
def from_file(path):
doc = Doc()
with open(path, "r") as f:
d = yaml_load(f)
doc._nodes = [Node.from_dict(i) for i in d["nodes"]]
return doc
def to_dict(self):
return yaml_dump(slim_dict({
"nodes": [node.to_dict() for node in self._nodes],
}))
def render(self):
result = []
nodes = [node for node in self._nodes]
while nodes:
first = self._first_node(nodes)
result.append(first)
nodes = [node for node in nodes if node != first]
return "".join([i.content() for i in result])
def _first_node(self, nodes):
for potential_root_node in sorted([node for node in nodes if not node.from_id()], key=lambda x: x.id()):
return potential_root_node
node_ids = [node.id() for node in nodes]
for potential_rootest_node in sorted([node for node in nodes if not node.from_id() in node_ids], key=lambda x: x.id()):
return potential_rootest_node
raise Exception(nodes)
class Node():
def __init__(self, client_name, ts, content, is_delete, from_id, to_id):
self._ts = ts
self._client_name = client_name
self._content = content
self._is_delete = is_delete
self._from_id = from_id
self._to_id = to_id
def __str__(self):
return str(self.to_dict())
def from_dict(d):
return Node(
d.get("client_name", ""),
d.get("ts", 0),
d.get("content", ""),
d.get("is_delete", False),
d.get("from", ""),
d.get("to", ""),
)
def to_dict(self):
return slim_dict({
"client_name": self._client_name,
"ts": self._ts,
"content": self._content,
"is_delete": self._is_delete,
"from": self._from_id,
"to": self._to_id,
})
def clone(self):
return Node(
self._ts,
self._client_name,
self._content,
self._is_delete,
self._from_id,
self._to_id,
)
def id(self):
return f'{self._ts}/{self._client_name}'
def from_id(self):
return self._from_id
def to_id(self):
return self._to_id
def content(self):
if self._is_delete:
self._content = ""
return self._content
def slim_dict(d):
return {k:v for k,v in d.items() if v or (isinstance(v, int) and not isinstance(v, bool))}
if __name__ == "__main__":
main()

View File

@ -0,0 +1,179 @@
from yaml import safe_load as yaml_load
from yaml import dump as yaml_dump
from sys import argv
def main():
path = "./testdata/hello_world.yaml"
if len(argv) > 1:
path = argv[1]
doc = Doc.from_file(path)
print(doc.render())
print(doc.to_dict())
# https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/
class Doc():
def __init__(self):
self._nodes = []
self._edges = []
def from_file(path):
doc = Doc()
with open(path, "r") as f:
d = yaml_load(f)
doc._nodes = [Node.from_dict(i) for i in d["nodes"]]
doc._edges = [Edge.from_dict(i) for i in d["edges"]]
return doc
def to_dict(self):
return yaml_dump(slim_dict({
"nodes": [node.to_dict() for node in self._nodes],
"edges": [edge.to_dict() for edge in self._edges],
}))
def render(self):
result = []
sorted_edges = self.sorted_edges()
for edge in sorted_edges:
result.extend([i for i in self._nodes if i.id() == edge.from_id()])
if sorted_edges:
result.extend([i for i in self._nodes if i.id() == sorted_edges[-1].to_id()])
for node in sorted(self._nodes, key=lambda x: x.id()):
if not node in result:
if result:
self._edges.append(Edge.between(result[-1], node))
result.append(node)
return "".join([i.content() for i in result])
def sorted_edges(self):
result = []
edges = [edge for edge in self._edges]
while edges:
first = self._first_edge(edges)
edges = [i for i in edges if i != first]
result.append(first)
return result
def _first_edge(self, edges):
to_ids = [edge.to_id() for edge in edges]
from_ids = [edge.from_id() for edge in edges]
candidates = []
for from_id in from_ids:
if not from_id in to_ids:
candidates.append(from_id)
from_edges = [edge for edge in edges if edge.from_id() in candidates]
return sorted(from_edges, key=lambda x: x.from_id())[0]
class Edge():
def __init__(self, from_client_name, from_ts, to_client_name, to_ts):
self._from_client_name = from_client_name
self._from_ts = from_ts
self._to_client_name = to_client_name
self._to_ts = to_ts
def between(a, b):
return Edge(
a.client_name(),
a.ts(),
b.client_name(),
b.ts(),
)
def from_dict(d):
return Edge(
d.get("from", {}).get("client_name", ""),
d.get("from", {}).get("ts", 0),
d.get("to", {}).get("client_name", ""),
d.get("to", {}).get("ts", 0),
)
def to_dict(self):
return slim_dict({
"from": slim_dict({
"client_name": self._from_client_name,
"ts": self._from_ts,
}),
"to": slim_dict({
"client_name": self._to_client_name,
"ts": self._to_ts,
}),
})
def from_id(self):
return f'{self._from_ts}/{self._from_client_name}'
def to_id(self):
return f'{self._to_ts}/{self._to_client_name}'
def __str__(self):
return f'{self._from_ts}/{self._from_client_name}..{self._to_ts}/{self._to_client_name}'
class Node():
def __init__(self, client_name, ts, content, is_delete):
self._ts = ts
self._client_name = client_name
self._content = content
self._is_delete = is_delete
def __str__(self):
return str(self.to_dict())
def from_dict(d):
return Node(
d.get("client_name", ""),
d.get("ts", 0),
d.get("content", ""),
d.get("is_delete", False),
)
def to_dict(self):
return slim_dict({
"client_name": self._client_name,
"ts": self._ts,
"content": self._content,
"is_delete": self._is_delete,
})
def clone(self):
return Node(
self._ts,
self._client_name,
self._content,
self._is_delete,
)
def id(self):
return f'{self._ts}/{self._client_name}'
def content(self):
if self.is_delete():
return ""
return self._content
def client_name(self):
return self._client_name
def ts(self):
return self._ts
def is_delete(self):
return self._is_delete
def split(self, idx):
up_to = self.clone()
up_to._content = up_to._content[:idx]
starting_at = self.clone()
starting_at._content = up_to._content[idx:]
return [up_to, starting_at]
def merge(self, other):
assert(self._client_name == other._client_name)
self._ts = max(self._ts, other._ts)
self._content += other._content
self._is_delete = self._is_delete or other._is_delete
return self
def slim_dict(d):
return {k:v for k,v in d.items() if v or (isinstance(v, int) and not isinstance(v, bool))}
if __name__ == "__main__":
main()

View File

@ -0,0 +1,18 @@
import unittest
import cdrt
class TestTestdata(unittest.TestCase):
def test(self):
from pathlib import Path
for path in Path('./testdata').rglob('*.yaml'):
doc = cdrt.Doc.from_file(path)
result = doc.render()
print(doc.to_dict())
with open(path, "r") as f:
d = cdrt.yaml_load(f)
if "expect" in d:
print(f'expect "{d["expect"]}", got "{result}"')
self.assertEqual(d["expect"], result)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,11 @@
expect: "hello world ohno"
nodes:
- ts: 0
content: hello
to: 1/
- ts: 1
content: " world"
from: 0/
- ts: 99
content: " ohno"
from: 0/

View File

@ -0,0 +1,6 @@
expect: "maybeAmaybeB"
nodes:
- ts: 0
content: "maybeA"
- ts: 0
content: "maybeB"

View File

@ -0,0 +1,10 @@
expect: "hello world"
nodes:
- ts: 0
content: hello
client_name: x
to: 1/x
- ts: 1
content: " world"
client_name: x
from: 0/x

View File

@ -0,0 +1,11 @@
expect: " world"
nodes:
- ts: 0
content: hello
client_name: x
to: 1/x
is_delete: true
- ts: 1
content: " world"
client_name: x
from: 0/x

View File

@ -0,0 +1,16 @@
expect: "idx0idx1idx2idx3"
nodes:
- ts: 1
content: "idx0"
to: 0/
- ts: 0
content: "idx1"
from: 1/
to: 2/
- ts: 2
content: "idx2"
from: 0/
to: 3/
- ts: 3
content: "idx3"
from: 2/

42
design.d/data-model.yaml Normal file
View File

@ -0,0 +1,42 @@
id: xyz
last_modified: 2006-01-02T03:04:05Z
state:
players:
oathsworn:
xyz:
hp: 0
tokens:
- xyz # defense, 2animus, redraw, Nempower, battleflow
animus:
regen: 0 # 6+-
max: 0 # 6+-
companions:
xyz: {}
free_company:
name: xyz
chapters:
- number: 1 # [1,21]
progress:
story:
complete: false
ambushed: false
special_rules: false
unique_item: false
encounter: false
traits:
- name: xyz # [0,3] stacks per name
knockouts:
- chapter: x
name: xyz
city:
- number: [0, 0]
allies:
- number: 0
name: xyz
deceased: true
wood:
- number: [0, 0]
notes:
- xyz
keywords:
- xyz