gogo
commit
6283d7f321
|
|
@ -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.
|
|
@ -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]
|
||||
|
|
@ -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 (don’t 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.
|
|
@ -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()
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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/
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
expect: "maybeAmaybeB"
|
||||
nodes:
|
||||
- ts: 0
|
||||
content: "maybeA"
|
||||
- ts: 0
|
||||
content: "maybeB"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue