diff --git a/README.md b/README.md
index 6287fe1..d5bfa76 100644
--- a/README.md
+++ b/README.md
@@ -5,16 +5,16 @@ The dictionary and toolkit for the **grid script**: how humans write down what R
**Docs:** https://interimm.github.io/glyph-cli/
```sh
-uvx --from git+https://github.com/InterImm/glyph-cli glyph show BODY.OTHER STAR.TIME ONE.137
+uvx --from git+https://github.com/InterImm/glyph-cli glyph show BODY.OTHER STAR.TIME COUNT.137
```
```text
+++.+++ .+..+.+ .....+.
-+++.+.. +++.+.+ .+....+
-+++.+++ .+..+.+ ......+
++++.+.. +++.+.+ ......+
++++.+++ .+..+.+ +++...+
BODY.OTHER: your world
STAR.TIME: pulsar
- ONE.137: 137
+ COUNT.137: 137
```
## Install
@@ -28,7 +28,7 @@ Python 3.10 or newer, no other dependencies.
## Use
```sh
-glyph parts # the 15 parts
+glyph parts # the 16 parts
glyph find wind # search meanings
glyph check BODY.STAR # is it a word? exit 1 if not
glyph show BODY.OTHER --symbol × # draw words
diff --git a/docs/assets/examples/how-many.drawing.txt b/docs/assets/examples/how-many.drawing.txt
index 7cd9cde..3453ae2 100644
--- a/docs/assets/examples/how-many.drawing.txt
+++ b/docs/assets/examples/how-many.drawing.txt
@@ -1,7 +1,7 @@
-.+..+.+..............+.
-+++.+.+..+.......+..+.+
-.+..+.+..............+.
+.+..+.+..........+.....
++++.+.+..+......+.+....
+.+..+.+..........+..+++
.......................
.......................
......................×
-....................×..
+................×××.×..
diff --git a/docs/assets/examples/how-many.svg b/docs/assets/examples/how-many.svg
index ab8b122..d4a0ba5 100644
--- a/docs/assets/examples/how-many.svg
+++ b/docs/assets/examples/how-many.svg
@@ -17,11 +17,11 @@
-
++
-+
+
+
+
@@ -39,13 +39,13 @@
-
-+
-
++
+
++
-+
+
-+
+
+
@@ -63,12 +63,12 @@
-
++
-
++
+
-
++
@@ -154,9 +154,9 @@
-
-
-
+×
+×
+×
×
diff --git a/docs/assets/examples/pulsars.drawing.txt b/docs/assets/examples/pulsars.drawing.txt
index 56a0557..6eccdb8 100644
--- a/docs/assets/examples/pulsars.drawing.txt
+++ b/docs/assets/examples/pulsars.drawing.txt
@@ -5,5 +5,5 @@
.......................
.......................
.+..+.+................
-+++.+.+..+.......+....+
-.+..+.+.............+..
++++.+.+..+............+
+.+..+.+.........+++.+..
diff --git a/docs/assets/examples/pulsars.svg b/docs/assets/examples/pulsars.svg
index 71a0941..b609a71 100644
--- a/docs/assets/examples/pulsars.svg
+++ b/docs/assets/examples/pulsars.svg
@@ -178,7 +178,7 @@
-+
+
@@ -200,9 +200,9 @@
-
-
-
++
++
++
+
diff --git a/docs/assets/parts.txt b/docs/assets/parts.txt
index 75082cd..bf22290 100644
--- a/docs/assets/parts.txt
+++ b/docs/assets/parts.txt
@@ -1,14 +1,19 @@
-SELF OTHER STAR LIGHT BODY
-### ### .#. #.# ###
-#.# #.. ### .#. ###
-### ### .#. #.# ###
+SELF OTHER STAR LIGHT
+### ### .#. #.#
+#.# #.. ### .#.
+### ### .#. #.#
-AIR WATER TIME VOICE PATH
-#.# ### #.# #.. ...
-... ... #.# ##. ###
-#.# ### #.# ### ...
+BODY AIR WATER TIME
+### #.# ### #.#
+### ... ... #.#
+### #.# ### #.#
-NOT ONE BEFORE AFTER OPEN
-#.. ... ... ... .#.
-.#. .#. #.. ..# #.#
-..# ... ... ... .#.
+VOICE PATH NOT ONE
+#.. ... #.. ...
+##. ### .#. .#.
+### ... ..# ...
+
+BEFORE AFTER OPEN COUNT
+... ... .#. ...
+#.. ..# #.# ...
+... ... .#. ###
diff --git a/docs/cli.md b/docs/cli.md
index 23da074..631a963 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -9,12 +9,12 @@ usage: glyph [-h] [--version] [--vocab PATH] COMMAND ...
glyph: the dictionary and toolkit for the grid script of Ross 128 b.
Words are written KIND.WHICH (BODY.OTHER = "your world"), or KIND alone.
-A number is ONE. (0-511). "_" is an empty position or an empty word.
+A number is COUNT. (0-511); COUNT alone is zero. "_" is an empty position or an empty word.
Pages are text files, one band per line: "SYMBOL: node | relation | node".
positional arguments:
COMMAND
- parts list the 15 parts and the relation markers
+ parts list the parts and the relation markers
list list the vocabulary
check is a word in the vocabulary? (exit 1 if not)
find search meanings and notes
@@ -43,7 +43,7 @@ Run `glyph init` to make an editable copy. Docs: https://interimm.github.io/glyp
$ glyph parts --help
usage: glyph parts [-h] [--json]
-list the 15 parts and the relation markers
+list the parts and the relation markers
options:
-h, --help show this help message and exit
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 33d4823..a9a05da 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -30,7 +30,7 @@ glyph --version
## Look at the parts
-Everything is built from 15 parts, each a 3×3 shape.
+Everything is built from 16 parts, each a 3×3 shape.
```sh
glyph parts
@@ -49,7 +49,7 @@ glyph check BODY.STAR # no, and its literal reading
glyph show BODY.OTHER BODY.SELF --symbol ×
```
-Numbers are built in: `ONE.137` is 137, drawn as nine bits.
+Numbers are built in: `COUNT.137` is 137, its which drawn as nine bits. `COUNT` alone is zero.
## Write a page
diff --git a/docs/index.md b/docs/index.md
index 4d8a903..dd18e66 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -13,16 +13,16 @@ It draws words and pages on the lattice, keeps the vocabulary honest, reads a pa
No install needed if you have [uv](https://docs.astral.sh/uv/):
```sh
-uvx --from git+https://github.com/InterImm/glyph-cli glyph show BODY.OTHER STAR.TIME ONE.137
+uvx --from git+https://github.com/InterImm/glyph-cli glyph show BODY.OTHER STAR.TIME COUNT.137
```
```text
+++.+++ .+..+.+ .....+.
-+++.+.. +++.+.+ .+....+
-+++.+++ .+..+.+ ......+
++++.+.. +++.+.+ ......+
++++.+++ .+..+.+ +++...+
BODY.OTHER: your world
STAR.TIME: pulsar
- ONE.137: 137
+ COUNT.137: 137
```
Then follow [Getting started](getting-started.md).
@@ -31,7 +31,7 @@ Then follow [Getting started](getting-started.md).
| Command | What for |
|---|---|
-| `glyph parts` | The 15 parts and what they mean as a thing and as a relation |
+| `glyph parts` | The 16 parts and what they mean as a thing and as a relation |
| `glyph list`, `find`, `check` | Look words up |
| `glyph show` | Draw words |
| `glyph render` | Draw a whole page, as text or as SVG pixels |
diff --git a/docs/pages.md b/docs/pages.md
index 7580057..09c6d0e 100644
--- a/docs/pages.md
+++ b/docs/pages.md
@@ -8,7 +8,7 @@ A page is a plain text file that `glyph render`, `glyph graph` and `glyph decode
- The first band of a block is the **statement**, one edge of the graph. Bands under it are **other voices** replying, part by part.
- A **blank line** starts the next statement.
- `#` starts a comment.
-- A word is `KIND.WHICH`, `KIND`, `_.WHICH`, or `_` for nothing. Numbers are `ONE.0` to `ONE.511` (or `_.12` in a reply).
+- A word is `KIND.WHICH`, `KIND`, `_.WHICH`, or `_` for nothing. Numbers are `COUNT.0` to `COUNT.511`; `COUNT` alone is zero (in a reply under a number, `_.12` changes just the bits).
- A symbol is any single character except a space, `.`, `#`, `|`, `:` and `_`. By convention `+` is Ross 128 b and `×` is Earth.
```text
diff --git a/docs/script.md b/docs/script.md
index 1ebc220..ebfd95d 100644
--- a/docs/script.md
+++ b/docs/script.md
@@ -41,6 +41,7 @@ This is the grammar `glyph` implements: version 7 of the grid script, where **a
| BEFORE | earlier | — |
| AFTER | later | — |
| OPEN | what goes here? | — |
+| COUNT | number | — |
## Relations
@@ -58,21 +59,21 @@ A relation is one part (the action) plus a which that marks time or polarity:
## Numbers
-`ONE` with a which read as nine bits, 256 down to 1, left to right and top to bottom. `ONE.137`:
+`COUNT` plus a which read as nine bits, 256 down to 1, left to right and top to bottom. `COUNT` alone is zero. `COUNT.137`:
```text
.....+.
-.+....+
......+
++++...+
```
-!!! note "Numbers that look like words"
- Some numbers spell a part's shape: 495 is drawn exactly like `SELF`, so `ONE.495` and `ONE.SELF` (*one of us*) look the same on the page. When a drawing is read back, `glyph decode` reads such a which as the vocabulary word, and `glyph check ONE.495` warns about it. The clashes today are 16 (`ONE.ONE`), 170 (`ONE.OPEN`), 273 (`ONE.NOT`), 487 (`ONE.OTHER`) and 495 (`ONE.SELF`). `ONE.0` draws nothing in the which, so it reads back as `ONE` alone.
+COUNT is the only kind whose which is read as bits, and it never takes a part as its which, so a number can never be drawn like a word. (Under the old rule, numbers were `ONE` plus bits, and 495 was drawn exactly like `ONE.SELF`, *one of us*.)
## "That" and questions
- `ONE` alone in a node slot means **that**: the statement on the line above.
- `OPEN` in any position asks **what goes here?** The asker leaves an empty band for the answer, and whatever is drawn under `OPEN` replaces it. `OPEN` as a relation's which asks yes or no.
+- **How many?** is `OPEN.COUNT` (*what number?*), answered with a `COUNT` word.
## How the machine reads a page
diff --git a/docs/vocabulary.md b/docs/vocabulary.md
index be6a3e0..7d0fd1f 100644
--- a/docs/vocabulary.md
+++ b/docs/vocabulary.md
@@ -1,7 +1,7 @@
# Vocabulary
How words are made: a **kind** part narrowed by a **which** part (`KIND.WHICH`). A single part is a word
-on its own. Any part can also be a **relation**, and numbers (`ONE.0` to `ONE.511`) are built in, so they
+on its own. Any part can also be a **relation**, and numbers (`COUNT.0` to `COUNT.511`) are built in, so they
are not listed. Status: **preliminary**, 55 words.
@@ -93,5 +93,5 @@ are not listed. Status: **preliminary**, 55 words.
| `.......`
`.+.....`
`.......` | ONE | that | alone in a node slot: the line above |
| `.......`
`.+...+.`
`.......` | ONE.ONE | the same | |
| `....+..`
`.+...+.`
`......+` | ONE.NOT | different | |
-| `.....+.`
`.+..+.+`
`.....+.` | ONE.OPEN | how many? | |
+| `.+.....`
`+.+....`
`.+..+++` | OPEN.COUNT | how many? | "what number?"; answer with COUNT.n |
| `.+.....`
`+.+....`
`.+.....` | OPEN | what? | |
diff --git a/examples/how-many.txt b/examples/how-many.txt
index f6a47a5..6f8e67b 100644
--- a/examples/how-many.txt
+++ b/examples/how-many.txt
@@ -1,3 +1,4 @@
# "How many pulsars?" Twelve.
-+: STAR.TIME | ONE | ONE.OPEN
-×: _ | _ | _.12
+# OPEN.COUNT asks "what number?"; the answer COUNT.12 replaces OPEN with COUNT, and COUNT with the bits.
++: STAR.TIME | ONE | OPEN.COUNT
+×: _ | _ | COUNT.12
diff --git a/examples/pulsars.txt b/examples/pulsars.txt
index 0c2d6ac..32e7099 100644
--- a/examples/pulsars.txt
+++ b/examples/pulsars.txt
@@ -2,4 +2,4 @@
# The same word on two lines is one node with two edges.
+: SELF | LIGHT.ONE | STAR.TIME
-+: STAR.TIME | ONE | ONE.12
++: STAR.TIME | ONE | COUNT.12
diff --git a/scripts/gen_docs.py b/scripts/gen_docs.py
index ab0a70d..5cdd6fa 100644
--- a/scripts/gen_docs.py
+++ b/scripts/gen_docs.py
@@ -35,11 +35,11 @@ def cli_markdown() -> str:
def parts_text(vocab: Vocabulary) -> str:
- """The 15 parts drawn in rows of five, with their names above."""
+ """The parts drawn in rows of four, with their names above."""
names = list(vocab.parts)
blocks = []
- for i in range(0, len(names), 5):
- row = names[i : i + 5]
+ for i in range(0, len(names), 4):
+ row = names[i : i + 4]
lines = ["".join(f"{n:<10}" for n in row).rstrip()]
for r in range(3):
lines.append("".join(f"{vocab.parts[n].shape[r]:<10}" for n in row).rstrip())
diff --git a/src/glyph_cli/__init__.py b/src/glyph_cli/__init__.py
index 621e445..c8f49a2 100644
--- a/src/glyph_cli/__init__.py
+++ b/src/glyph_cli/__init__.py
@@ -2,6 +2,6 @@
from .script import Entry, GlyphError, Part, Vocabulary, Word
-__version__ = "0.1.0"
+__version__ = "0.2.0"
__all__ = ["Entry", "GlyphError", "Part", "Vocabulary", "Word", "__version__"]
diff --git a/src/glyph_cli/cli.py b/src/glyph_cli/cli.py
index bb865ed..e9c8bf8 100644
--- a/src/glyph_cli/cli.py
+++ b/src/glyph_cli/cli.py
@@ -14,7 +14,7 @@
from .export import vocabulary_markdown
from .graph import format_graph, read_graph, to_dot
from .page import check_symbol, format_page, parse_page
-from .script import GlyphError, Vocabulary, Word
+from .script import NUMBER, GlyphError, Vocabulary
ENV_VOCAB = "GLYPH_VOCAB"
@@ -22,7 +22,7 @@
glyph: the dictionary and toolkit for the grid script of Ross 128 b.
Words are written KIND.WHICH (BODY.OTHER = "your world"), or KIND alone.
-A number is ONE. (0-511). "_" is an empty position or an empty word.
+A number is COUNT. (0-511); COUNT alone is zero. "_" is an empty position or an empty word.
Pages are text files, one band per line: "SYMBOL: node | relation | node".
"""
@@ -56,13 +56,6 @@ def emit_json(data: object) -> None:
print(json.dumps(data, indent=2, ensure_ascii=False))
-def warn_twin(v: Vocabulary, word: Word) -> None:
- if word.is_number:
- twin = v.number_twin(word.which) # type: ignore[arg-type]
- if twin:
- print(f' note: drawn exactly like {twin.word} ("{twin.gloss}"); a reader sees that word', file=sys.stderr)
-
-
# ---------- commands ----------
@@ -105,9 +98,8 @@ def cmd_check(v: Vocabulary, a: argparse.Namespace) -> int:
if e:
print(f'yes: {word} = "{e.gloss}" ({e.domain})')
return 0
- if word.kind == "ONE" and word.is_number:
- print(f"yes: {word} = the number {word.which} (numbers are built in)")
- warn_twin(v, word)
+ if word.kind == NUMBER:
+ print(f"yes: {word} = the number {word.which or 0} (numbers are built in)")
return 0
print(f"no: {word} is not in the vocabulary. Literal reading: {v.gloss(word)}")
same_kind = [x for x in v.entries if x.word.kind == word.kind]
@@ -133,7 +125,6 @@ def cmd_show(v: Vocabulary, a: argparse.Namespace) -> int:
print("\n".join(draw_words(v, words, check_symbol(a.symbol))))
for w in words:
print(f" {w}: {v.gloss(w)}")
- warn_twin(v, w)
return 0
@@ -215,7 +206,7 @@ def add(name: str, func: Callable, help: str) -> argparse.ArgumentParser:
p.set_defaults(func=func)
return p
- p = add("parts", cmd_parts, "list the 15 parts and the relation markers")
+ p = add("parts", cmd_parts, "list the parts and the relation markers")
p.add_argument("--json", action="store_true", help="print JSON")
p = add("list", cmd_list, "list the vocabulary")
p.add_argument("--domain", help="only domains containing this text")
diff --git a/src/glyph_cli/data/vocab.json b/src/glyph_cli/data/vocab.json
index 26621e9..fa3d907 100644
--- a/src/glyph_cli/data/vocab.json
+++ b/src/glyph_cli/data/vocab.json
@@ -135,6 +135,15 @@
],
"thing": "what goes here?",
"relation": null
+ },
+ "COUNT": {
+ "shape": [
+ "...",
+ "...",
+ "###"
+ ],
+ "thing": "number",
+ "relation": null
}
},
"markers": {
@@ -519,11 +528,11 @@
"note": ""
},
{
- "kind": "ONE",
- "which": "OPEN",
+ "kind": "OPEN",
+ "which": "COUNT",
"gloss": "how many?",
"domain": "Logic and number",
- "note": ""
+ "note": "\"what number?\"; answer with COUNT.n"
},
{
"kind": "OPEN",
diff --git a/src/glyph_cli/drawing.py b/src/glyph_cli/drawing.py
index c729fad..61bf1f4 100644
--- a/src/glyph_cli/drawing.py
+++ b/src/glyph_cli/drawing.py
@@ -10,7 +10,7 @@
from html import escape
from .page import Band, Line, Page, check_symbol
-from .script import GlyphError, Half, Vocabulary, Word
+from .script import NUMBER, GlyphError, Half, Vocabulary, Word
POSITIONS = 6
WIDTH = POSITIONS * 4 - 1 # 23 cells
@@ -136,24 +136,19 @@ def _structure(rows: list[str]) -> int:
raise GlyphError("the rows don't fall into lines and bands; is this a picture rather than text?")
-def _which(vocab: Vocabulary, pattern: tuple[str, ...], kind: Half, slot: int, statement_kind: Half) -> Half:
- by_shape = {p.shape: p.name for p in vocab.parts.values()}
- named = by_shape.get(pattern)
- numeric = slot != 1 and (kind == "ONE" or (kind is None and statement_kind == "ONE"))
- if numeric:
- if named and (named == "OPEN" or vocab.lookup(Word("ONE", named))):
- return named
- bits = "".join(pattern).replace(EMPTY_CELL, "0").replace(MARK, "1")
- return int(bits, 2)
- return named
+def _which(vocab: Vocabulary, pattern: tuple[str, ...], kind: Half, above: Word | None) -> Half:
+ """Read a which: bits under COUNT (or under nothing, answering a COUNT word above), else a part."""
+ if kind == NUMBER or (kind is None and above is not None and NUMBER in (above.kind, above.which)):
+ return int("".join(pattern).replace(EMPTY_CELL, "0").replace(MARK, "1"), 2)
+ return {p.shape: p.name for p in vocab.parts.values()}.get(pattern)
def decode(vocab: Vocabulary, text: str) -> Page:
"""Read a drawing (as made by :func:`render`) back into a page.
- Ambiguity: in a node slot whose kind is ONE, a which that is both a part
- and a number reads as the part only when ONE.PART is a vocabulary word (or
- OPEN, "how many?"); otherwise it reads as the number.
+ A which is read as nine bits only after COUNT, or with no kind under a line
+ whose word there is a COUNT word (a reply like ``_.12``); everywhere else it
+ must be a part.
"""
rows = [r.strip() for r in text.splitlines() if r.strip()]
if not rows:
@@ -195,8 +190,8 @@ def decode(vocab: Vocabulary, text: str) -> Page:
raise GlyphError(f"rows {r0 + 1}-{r0 + 3}, position {pos + 1}: not a part")
halves.append(name)
else:
- statement_kind = line.bands[0].words[slot].kind if line.bands else None
- which = _which(vocab, pattern, halves[-1], slot, statement_kind)
+ above = line.bands[0].words[slot] if line.bands else None
+ which = _which(vocab, pattern, halves[-1], above)
if which is None:
raise GlyphError(f"rows {r0 + 1}-{r0 + 3}, position {pos + 1}: not a part or a number here")
halves.append(which)
diff --git a/src/glyph_cli/export.py b/src/glyph_cli/export.py
index a60bf02..2e8fb8a 100644
--- a/src/glyph_cli/export.py
+++ b/src/glyph_cli/export.py
@@ -9,7 +9,7 @@
INTRO = """\
How words are made: a **kind** part narrowed by a **which** part (`KIND.WHICH`). A single part is a word
-on its own. Any part can also be a **relation**, and numbers (`ONE.0` to `ONE.511`) are built in, so they
+on its own. Any part can also be a **relation**, and numbers (`COUNT.0` to `COUNT.511`) are built in, so they
are not listed. Status: **preliminary**, {count} words."""
diff --git a/src/glyph_cli/script.py b/src/glyph_cli/script.py
index fec5496..243a232 100644
--- a/src/glyph_cli/script.py
+++ b/src/glyph_cli/script.py
@@ -2,8 +2,9 @@
A *part* is a 3x3 shape with a meaning. A *word* is two lattice positions,
a **kind** on the left and a **which** on the right, written ``KIND.WHICH``.
-Either half may be empty (``_``). When the kind is ``ONE`` the which may be a
-number from 0 to 511, drawn as nine bits.
+Either half may be empty (``_``). Numbers are ``COUNT.`` (0 to 511): COUNT is the
+only kind whose which is read as nine bits, and it never takes a part as its which,
+so a number can never be drawn like a word. ``COUNT`` alone is zero.
"""
from __future__ import annotations
@@ -19,6 +20,7 @@
MAX_NUMBER = 511
EMPTY = "_"
+NUMBER = "COUNT" # the kind whose which is a number
class GlyphError(Exception):
@@ -143,7 +145,7 @@ def save(self, path: str | os.PathLike | None = None) -> Path:
# ---- words ----
def parse(self, text: str) -> Word:
- """``'BODY.OTHER'`` -> ``Word('BODY', 'OTHER')``; ``'ONE.137'`` -> ``Word('ONE', 137)``."""
+ """``'BODY.OTHER'`` -> ``Word('BODY', 'OTHER')``; ``'COUNT.137'`` -> ``Word('COUNT', 137)``."""
s = text.strip()
if s in ("", EMPTY):
return Word()
@@ -156,10 +158,14 @@ def parse(self, text: str) -> Word:
which = int(w)
if which > MAX_NUMBER:
raise GlyphError(f"number out of range 0-{MAX_NUMBER}: {which}")
- if kind not in ("ONE", None):
- raise GlyphError(f"a number needs ONE as its kind, e.g. ONE.{which} (got {s})")
+ if kind not in (NUMBER, None):
+ raise GlyphError(f"a number needs {NUMBER} as its kind, e.g. {NUMBER}.{which} (got {s})")
+ if kind == NUMBER and which == 0:
+ which = None # COUNT alone is zero: COUNT.0 draws exactly like it
else:
which = w.upper()
+ if kind == NUMBER:
+ raise GlyphError(f"{NUMBER} only takes a number as its which, e.g. {NUMBER}.12 (got {s})")
for half in (kind, which):
if isinstance(half, str) and half not in self.parts:
raise GlyphError(f"unknown part: {half} (see `glyph parts`)")
@@ -192,8 +198,8 @@ def gloss(self, word: Word, role: str = "node") -> str:
return base
marker = self.markers.get(str(word.which), str(word.which))
return f"{base} [{marker}]"
- if word.kind == "ONE" and word.is_number:
- return str(word.which)
+ if word.kind == NUMBER and (word.is_number or word.which is None):
+ return str(word.which or 0)
entry = self.lookup(word)
if entry:
return entry.gloss
@@ -204,8 +210,8 @@ def gloss(self, word: Word, role: str = "node") -> str:
# ---- editing ----
def add(self, word: Word, gloss: str, domain: str = "Unsorted", note: str = "", force: bool = False) -> Entry:
- if word.kind is None or word.is_number:
- raise GlyphError("a vocabulary word needs a kind; numbers are built in")
+ if word.kind is None or word.is_number or word.kind == NUMBER:
+ raise GlyphError(f"a vocabulary word needs a kind; numbers ({NUMBER}.n) are built in")
existing = self.lookup(word)
if existing:
raise GlyphError(f'already in the vocabulary: {word} = "{existing.gloss}"')
@@ -247,6 +253,8 @@ def validate(self) -> list[str]:
for half in e.word.halves:
if isinstance(half, str) and half not in self.parts:
errors.append(f"{e.word}: unknown part {half}")
+ if e.word.kind == NUMBER:
+ errors.append(f"{e.word}: {NUMBER} words are numbers and are built in")
if e.word in seen:
errors.append(f'{e.word} defined twice ("{seen[e.word]}" and "{e.gloss}")')
seen[e.word] = e.gloss
@@ -256,17 +264,6 @@ def validate(self) -> list[str]:
glosses[g] = e.word
return errors
- def number_twin(self, n: int) -> Entry | None:
- """The ONE.PART word that is drawn exactly like the number ONE.n, if there is one.
-
- A number's nine bits can spell a part's shape (495 is SELF), so ONE.495 and
- ONE.SELF ("one of us") look the same on the page.
- """
- for p in self.parts.values():
- if int("".join(p.shape).replace("#", "1").replace(".", "0"), 2) == n:
- return self.lookup(Word("ONE", p.name))
- return None
-
def domains(self) -> list[str]:
out: list[str] = []
for e in self.entries:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 60b81a8..6442a33 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -24,7 +24,7 @@ def test_parts(capsys):
code, out, _ = run(capsys, "parts")
assert code == 0 and "SELF" in out and "asked: does it?" in out
code, out, _ = run(capsys, "parts", "--json")
- assert len(json.loads(out)["parts"]) == 15
+ assert len(json.loads(out)["parts"]) == 16
def test_list(capsys):
@@ -39,7 +39,7 @@ def test_list(capsys):
def test_check(capsys):
assert run(capsys, "check", "BODY.OTHER")[0] == 0
- assert run(capsys, "check", "ONE.137")[0] == 0
+ assert run(capsys, "check", "COUNT.137")[0] == 0
code, out, _ = run(capsys, "check", "LIGHT.VOICE")
assert code == 1 and "not in the vocabulary" in out
code, _, err = run(capsys, "check", "WIND")
diff --git a/tests/test_pages.py b/tests/test_pages.py
index 3ae3ce5..e6b5240 100644
--- a/tests/test_pages.py
+++ b/tests/test_pages.py
@@ -20,14 +20,19 @@ def page(vocab, text):
+.+......+..+...+++.+..
+++.....+.+.....+++.+++
""",
- "+: STAR.TIME | ONE | ONE.OPEN\n×: _ | _ | _.12": """
-.+..+.+..............+.
-+++.+.+..+.......+..+.+
-.+..+.+..............+.
+ "+: STAR.TIME | ONE | COUNT.12": """
+.+..+.+................
++++.+.+..+............+
+.+..+.+.........+++.+..
+""",
+ "+: STAR.TIME | ONE | OPEN.COUNT\n×: _ | _ | COUNT.12": """
+.+..+.+..........+.....
++++.+.+..+......+.+....
+.+..+.+..........+..+++
.......................
.......................
......................×
-....................×..
+................×××.×..
""",
"+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR": """
+++.+++.........+++.+.+
@@ -67,22 +72,20 @@ def test_decode_round_trips_examples(vocab, path):
assert decode(vocab, "\n".join(render(vocab, p))) == p
-@pytest.mark.parametrize("n", [0, 1, 12, 137, 186, 511])
+@pytest.mark.parametrize("n", [0, 1, 12, 16, 137, 170, 273, 487, 495, 511])
def test_decode_numbers(vocab, n):
- p = page(vocab, f"+: STAR.TIME | ONE | ONE.{n}")
+ # Numbers whose bits spell a part's shape (495 is SELF) read back as numbers: they sit under COUNT.
+ p = page(vocab, f"+: STAR.TIME | ONE | COUNT.{n}")
back = decode(vocab, "\n".join(render(vocab, p)))
- # ONE.0 draws nothing in the which, so it reads back as ONE alone ("that").
- assert str(back.lines[0].statement.words[2]) == ("ONE" if n == 0 else f"ONE.{n}")
+ assert str(back.lines[0].statement.words[2]) == ("COUNT" if n == 0 else f"COUNT.{n}")
-@pytest.mark.parametrize("n, twin", [(495, "ONE.SELF"), (487, "ONE.OTHER"), (273, "ONE.NOT"), (16, "ONE.ONE")])
-def test_numbers_drawn_like_words_read_as_the_word(vocab, n, twin):
- assert str(vocab.number_twin(n).word) == twin
- p = page(vocab, f"+: STAR.TIME | ONE | ONE.{n}")
- assert str(decode(vocab, "\n".join(render(vocab, p))).lines[0].statement.words[2]) == twin
+def test_decode_kindless_number_reply(vocab):
+ p = page(vocab, "+: STAR.TIME | ONE | COUNT.12\n×: _ | _ | _.495")
+ assert decode(vocab, "\n".join(render(vocab, p))) == p
-def test_decode_prefers_vocabulary_words(vocab):
+def test_decode_words(vocab):
p = page(vocab, "+: ONE.SELF | ONE | ONE.OTHER")
assert decode(vocab, "\n".join(render(vocab, p))) == p
diff --git a/tests/test_script.py b/tests/test_script.py
index 20b5acc..73520e5 100644
--- a/tests/test_script.py
+++ b/tests/test_script.py
@@ -4,7 +4,7 @@
def test_bundled_vocabulary_is_sound(vocab):
- assert len(vocab.parts) == 15
+ assert len(vocab.parts) == 16
assert vocab.validate() == []
@@ -14,7 +14,9 @@ def test_bundled_vocabulary_is_sound(vocab):
("BODY.OTHER", Word("BODY", "OTHER")),
("body.other", Word("BODY", "OTHER")),
("SELF", Word("SELF", None)),
- ("ONE.137", Word("ONE", 137)),
+ ("COUNT.137", Word("COUNT", 137)),
+ ("COUNT.0", Word("COUNT", None)),
+ ("COUNT", Word("COUNT", None)),
("_.12", Word(None, 12)),
("_.WATER", Word(None, "WATER")),
("_", Word()),
@@ -25,20 +27,22 @@ def test_parse(vocab, text, word):
assert vocab.parse(text) == word
-@pytest.mark.parametrize("text", ["BODY.WIND", "ONE.512", "STAR.12", "FOO"])
+@pytest.mark.parametrize("text", ["BODY.WIND", "COUNT.512", "STAR.12", "ONE.12", "COUNT.SELF", "FOO"])
def test_parse_rejects(vocab, text):
with pytest.raises(GlyphError):
vocab.parse(text)
-@pytest.mark.parametrize("text", ["BODY.OTHER", "SELF", "ONE.137", "_.12", "_.WATER", "_"])
+@pytest.mark.parametrize("text", ["BODY.OTHER", "SELF", "COUNT.137", "COUNT", "_.12", "_.WATER", "_"])
def test_word_str_round_trips(vocab, text):
assert str(vocab.parse(text)) == text
def test_gloss(vocab):
assert vocab.gloss(Word("BODY", "OTHER")) == "your world"
- assert vocab.gloss(Word("ONE", 137)) == "137"
+ assert vocab.gloss(Word("COUNT", 137)) == "137"
+ assert vocab.gloss(Word("COUNT", None)) == "0"
+ assert vocab.gloss(Word("OPEN", "COUNT")) == "how many?"
assert vocab.gloss(Word("LIGHT", "BEFORE"), role="relation") == "see [past]"
assert vocab.gloss(Word("ONE", None), role="relation") == "is, equals"
assert vocab.gloss(Word("LIGHT", "VOICE")).endswith("(not in vocabulary)")
@@ -60,7 +64,7 @@ def test_add_and_remove(vocab):
def test_add_rejects_numbers_and_kindless(vocab):
- for w in (Word("ONE", 5), Word(None, "WATER")):
+ for w in (Word("COUNT", 5), Word("COUNT", None), Word(None, "WATER")):
with pytest.raises(GlyphError):
vocab.add(w, "x")
@@ -78,6 +82,20 @@ def test_save_and_load(vocab, tmp_path):
assert again.path == path
+def test_validate_rejects_count_words(vocab):
+ data = vocab.to_json()
+ data["words"].append({"kind": "COUNT", "which": 3, "gloss": "three", "domain": "x", "note": ""})
+ assert any("built in" in e for e in Vocabulary.from_json(data).validate())
+
+
+def test_no_number_draws_like_a_word(vocab):
+ """The reason for COUNT: no COUNT.n looks like any vocabulary word."""
+ from glyph_cli.drawing import band_rows
+
+ words = {tuple(band_rows(vocab, list(e.word.halves), "#")) for e in vocab.entries}
+ assert all(tuple(band_rows(vocab, ["COUNT", n], "#")) not in words for n in range(1, 512))
+
+
def test_validate_catches_duplicate_shapes(vocab):
data = vocab.to_json()
data["parts"]["AFTER"]["shape"] = data["parts"]["BEFORE"]["shape"]