Skip to content

jengine-kotoba

Word tiles: kana placed one at a time where they make a word, read across or down - a timed puzzle, a real-time word battle with combos, a bot, and a Japanese dictionary with meanings.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-kotoba
Version0.1.0
Entry pointjengine_kotoba::KotobaPackage
Script namespaceKotoba
Depends onjengine-online
Listed byprojects/kotodama

List it in the top-level "packages" array of project.jengine, together with every package it depends on: a crate dependency compiles a package in but does not register it, and packages_a_project_lists_cover_their_dependencies fails a project that lists only the outer one:

{
"packages": [
{ "name": "jengine-kotoba", "source": { "type": "path", "path": "../../packages/jengine-kotoba" } },
{ "name": "jengine-online", "source": { "type": "path", "path": "../../packages/jengine-online" } },
{ "name": "jengine-net", "source": { "type": "path", "path": "../../packages/jengine-net" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Kotoba == nil then return end.

Scene keyRust type
kotoba_tableTablepackage only: needs a v2 scene
kotoba_wavesWavespackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Kotoba.puzzle(e, stage)kotoba_puzzle
Kotoba.battle(e, rival)kotoba_battle
Kotoba.demo(e)kotoba_demo
Kotoba.stop(e)kotoba_stop
Kotoba.pause(e, paused)kotoba_pause
Kotoba.press(e, x, y)kotoba_press
Kotoba.drag(e, x, y)kotoba_drag
Kotoba.release(e, x, y)kotoba_release
Kotoba.select(e, slot)kotoba_select
Kotoba.turn(e)kotoba_turn
Kotoba.cursor(e, dx, dy)kotoba_cursor
Kotoba.place(e)kotoba_place
Kotoba.swap(e)kotoba_swap
Kotoba.hint(e)kotoba_hint
Kotoba.online(e, conn)kotoba_online
Kotoba.ripple(e, x, y, strength, r, g, b)kotoba_ripple
Kotoba.surge(e, strength, r, g, b)kotoba_surge
Kotoba.swell(e, amount)kotoba_swell
  • projects/kotodama/assets/scenes/battle.scene
  • projects/kotodama/assets/scenes/howto.scene
  • projects/kotodama/assets/scenes/online.scene
  • projects/kotodama/assets/scenes/puzzle.scene
  • projects/kotodama/assets/scenes/results.scene
  • projects/kotodama/assets/scenes/rivals.scene
  • projects/kotodama/assets/scenes/stages.scene
  • projects/kotodama/assets/scenes/title.scene

From packages/jengine-kotoba/README.md.

Word tiles: kana go on a board one at a time, and a tile only stays where it makes a word - read left to right, or top to bottom. The game of Kotoba no Puzzle: Mojipittan, with a real-time two-player battle on top. It is what Kotodama (projects/kotodama) is made of.

A tile may go on an empty cell next to a kana, if a word then reads through it. The word need not be the whole run of kana it sits in: a board fills up with runs that are not words themselves (さかなつ), and placing the な still counts because さかな is one. Each direction keeps its longest words, so かな inside さかな is not scored twice. A word is two kana or more.

A tile in hand can be turned through its forms - か → が, は → ば → ぱ, つ → づ → っ, や → ゃ - and when the face it shows makes no word, placing it tries its other forms by itself.

Points are the kana’s values times one less than the word’s length, so length multiplies; a tile that reads both ways at once scores half again. What a kana is worth and how often the bag deals it are read off the dictionary (kana::TileStats): a kana the words use often is common and cheap, a rare one is rare and dear (い 1, ぬ 4), and a kana no word uses (を) is never dealt.

ModuleWhat it is
kanathe tiles, their forms, and the bag weights and values
dictthe dictionary - words/ja.tsv, compiled in: reading, written form, meaning
boardthe grid and the rule: Board::words_at
playwhat both modes share: hands, stocks, placing, the events
puzzleone player, a stage, a stock to empty against the clock
battletwo players at once on one board, every word a hit
botevery legal move, and one picked by skill
drawthe board on screen, in ordinary components
contentstages and rivals: the project’s data tables, or a built-in one of each
wavesthe sea behind the board: seigaiha built from its pieces, swelling and rippling
neta battle over the network: the wire format a server and a client share, prediction and reconciliation

None of the rules knows about the ECS; draw and lib.rs are the seam.

A stage (data/kotoba_stages) is a board drawn in text - . a cell, # a wall, a kana a kana already there - a stock of tiles dealt from seed, a hand size, a time and the stars scores. Clear it by placing every tile. A swap trades the chosen tile for another (5 s; once the stock is dry, for a fresh tile from the seed), so a hand is never a dead end - only the clock is. A hint chooses a tile and lights its cell (10 s). Time left is a bonus. puzzle::autoplay plays a stage with the bot; a test plays every shipped stage through it and fails one the bot cannot clear.

Two Fighters on one board, with no turns. A word hits the other player for its points; words within COMBO_WINDOW (4 s) chain, each step adding a quarter, up to double. A swap stuns for two seconds. A knockout, the clock (more life wins) or a full board ends it.

Battle::apply(dict, who, Move) takes a move, Battle::update(dict, dt) moves time and the bots on. A Move is plain data (Place, Turn, Swap), applied in the order it arrives - which is what an online match needs.

net is a battle over the network, written once for both ends. A server room (projects/kotodama/server) owns the Battle, applies moves in the order they reach it - two players racing for one cell are settled by whose move it heard first - and sends match (a net::Start), state (a net::State, ten times a second and after every move), ev (the battle’s events), again and left. A client keeps a mirror built from the same Start and overwritten by each state; its own placements are predicted - down on the mirror the moment they are played (net::predict_place) and played again on top of every state until one acknowledges their number (net::apply_state), so a tile never blinks while its move is in flight and one the server refused (the other player took the cell first) is simply gone in the next state. Turning a tile is the player’s own business: it is not sent, and a placement carries the form it was played as.

The package does the client side itself: Kotoba.online(e, conn) names the connection entity (an online component, jengine-online), after which the package reads the server’s frames from its online bag, builds the mirror on match, and sends moves through its queue. So a project using it lists jengine-online and jengine-net beside this package. The lobby - connect, wait, rematch - is the scene’s script, talking to the room with Online.send.

One kotoba_table on a manager entity says where things are drawn: board_centre and board_max (the box the board fits; cell is the largest a cell may be), hand_centre / hand_tile for the player’s hand, rival_hand_centre / rival_tile / show_rival_hand for a battle rival’s, font (a TTF / OTF with the kana), glyph_centre and kana_size (where a kana sits on its tile), and the textures - tile, cell_tex, frame, panel, white - with their nine-slice borders. The textures are named only at runtime, so preload them (Kotodama puts them in a sprite_library on the same entity).

kotoba_waves is the background: seigaiha (青海波), the old pattern of overlapping waves, built from its pieces - every scale a sprite of its own (texture, drawn light so the tint colours it), laid in the pattern’s rows over area about centre - so it can move. It is never still:

  • at rest two slow waves cross it (swell, swell_speed, swell_length), each scale lifting and brightening between deep and light as a crest passes; scales glint like sun on water (glints a second); a gust rolls in from one side every so often (gusts, the least and most seconds between); and with bpm set it breathes on the music’s beat, read from the music’s own playhead, and holds still when the music does;
  • under a hand (hover) scales rise round the pointer, a quick sweep leaves a wake of small rings, and a press on the open sea splashes; a pointer at rest is forgotten after a moment, so a finger lifted from a phone leaves nothing behind;
  • the game sends rings and jolts. Every word sends a ring out from its tile (gold for player 1, coral for player 2; stronger for more points and a longer combo, an extra ring for each kana past three, a surge for a combo of three). A ring lights, lifts, tints and pushes outward the scales it passes, and fades as it spreads. A hit jolts the sea under the hurt player’s hand, harder for more damage, and your own hurt shows there in red; a refused tile sends a dark ring and a shudder; the countdown beats once a second and “go” lands like a drum; a clear sends three golden rings and a surge, a loss a dark surge, a knockout rings and a surge in the winner’s colour and a blow to the loser’s side.

lib.rs turns the board’s events into rings and jolts (waves_for), so a scene only needs the component; scripts add their own with the commands below. Lay the area wider than the screen - a wider screen shows more world.

Tiles in hand float on it, too: each bobs and rocks a little on a beat of its own, and the chosen one’s glow pulses.

CallDoes
Kotoba.puzzle(e, stage)play a stage by id
Kotoba.battle(e, rival)fight a rival by id: you against its bot
Kotoba.demo(e)two bots fighting, restarted when it ends (a title screen)
Kotoba.stop(e)clear the board
Kotoba.pause(e, paused)stop the clock and hide the kana
Kotoba.press(e, x, y) / drag(e, x, y) / release(e, x, y)a pointer at a world point: a hand tile is chosen (a second tap turns it), a cell takes the chosen tile, a drag carries a tile to a cell
Kotoba.select(e, slot)choose hand tile slot (from 1)
Kotoba.turn(e) / Kotoba.swap(e)turn or swap the chosen tile
Kotoba.cursor(e, dx, dy) / Kotoba.place(e)the keyboard cursor (the first call only shows it), and put the chosen tile under it
Kotoba.hint(e)a puzzle’s hint
Kotoba.online(e, conn)play the battles the server behind this connection entity runs (-1 to stop)
Kotoba.ripple(e, x, y, strength, r, g, b)on a kotoba_waves: a ring from a world point (sRGB colour, gold without one; negative strength darkens)
Kotoba.surge(e, strength, r, g, b)a wave across the whole sea
Kotoba.swell(e, amount)how rough the resting sea is (0 still, 1 gentle, up to 4)

The kotoba bag: active, mode (puzzle, battle, none), demo, paused, phase, selected (from 1; -1 none), hand (the faces), made (every word: reading, written, meaning, points, player), words (the dictionary’s size), stages and rivals (the tables, for menus). A puzzle adds stage, stage_name, stage_title, time, time_left, score, bonus, tiles_left, stock, swaps, stars, star_scores; a battle countdown, time_left, winner (from 1; -1 a draw), you, rival and fighters (name, hp, max_hp, score, combo, multiplier, combo_left, stun, words, best, hand); online, also online, seat (from 1), board (its name) and pending (placements not yet answered).

events, this frame’s: started, countdown (n), placed (player, x, y, kana, points, damage, combo, words - each with its cells), refused (why: no word, not next to a kana, taken), turned, swapped, hit (to, damage, hp), cleared (score, bonus, time_left), failed (why: time, stuck), over (winner, why: ko, time, full).

words/ja.tsv is written for this package (no third-party list, so no licence of its own): about two thousand everyday words - animals, food, the body, nature, home, school, town, people, time, verbs, adjectives, onomatopoeia and borrowed words in hiragana with ー. A test fails a line whose reading is not tile kana, or a reading that appears twice.