The first tap (or SHUFFLE) loads the solved board and deals it with a Fisher-Yates pass driven by the free-running logistic-map RNG, so tap timing picks the board. A parity latch adds one swap of cells 1 and 2 when needed, so every deal is solvable. Tile moves and deal swaps share one cell-swap mechanism, cutting RAM from 56.4 kB (failed to start on the Orion) to 34.6 kB (runs). Adds deal checks to verify_logic.py --v2 and the hint label to the viewer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
7.1 KiB
Java
177 lines
7.1 KiB
Java
import java.awt.*;
|
|
import java.net.InetAddress;
|
|
import javax.swing.*;
|
|
import sedona.*;
|
|
import sedona.dasp.DaspSocket;
|
|
import sedona.sox.*;
|
|
|
|
/**
|
|
* Live viewer for the aspirin apps running in the Ontrol simulator (headless SVM).
|
|
* Connects over SOX, polls /ox/Game widgets 5x per second and draws the 800x480 Orion page.
|
|
* Usage: java -cp ".;sedona.jar" OxViewer [host] [port] [user] [password]
|
|
*/
|
|
public class OxViewer extends JPanel {
|
|
static final String[] LABELS = {"Title", "Sub", "Moves", "Shuffle", "Busy", "Solving", "Hint", "Solved"};
|
|
SoxClient client;
|
|
SoxComponent page;
|
|
SoxComponent[] tiles = new SoxComponent[16];
|
|
SoxComponent[] cells = new SoxComponent[17]; // aspirinV2: fixed cells Cell01..Cell16
|
|
boolean v2;
|
|
SoxComponent[] labels = new SoxComponent[LABELS.length];
|
|
String lastBoard = "";
|
|
|
|
OxViewer(SoxClient client) throws Exception {
|
|
this.client = client;
|
|
SoxComponent app = client.loadApp();
|
|
page = load(app, "ox", "Game");
|
|
v2 = load(page, "Cell01") != null;
|
|
if (v2) for (int k = 1; k <= 16; k++) cells[k] = load(page, String.format("Cell%02d", k));
|
|
else for (int t = 1; t <= 15; t++) tiles[t] = load(page, String.format("Tile%02d", t));
|
|
for (int i = 0; i < LABELS.length; i++) labels[i] = load(page, LABELS[i]);
|
|
setPreferredSize(new Dimension(800, 480));
|
|
}
|
|
|
|
SoxComponent load(SoxComponent from, String... path) throws Exception {
|
|
SoxComponent c = from;
|
|
for (String p : path) {
|
|
if (c == null) return null;
|
|
client.update(c, 0x01); // tree
|
|
c = c.child(p);
|
|
}
|
|
if (c != null) client.update(c, 0x06); // config + runtime
|
|
return c;
|
|
}
|
|
|
|
void poll() throws Exception {
|
|
for (SoxComponent c : tiles) if (c != null) client.update(c, 0x06);
|
|
for (SoxComponent c : cells) if (c != null) client.update(c, 0x06);
|
|
for (SoxComponent c : labels) if (c != null) client.update(c, 0x06);
|
|
String b = boardText();
|
|
if (!b.equals(lastBoard)) {
|
|
System.out.println(b + " moves=" + str(labels[2], "in") + " " + status());
|
|
System.out.flush();
|
|
lastBoard = b;
|
|
}
|
|
}
|
|
|
|
static Value val(SoxComponent c, String slot) {
|
|
Slot s = c.type.slot(slot);
|
|
return s == null ? null : c.get(s);
|
|
}
|
|
static int i(SoxComponent c, String slot) { return Integer.parseInt(val(c, slot).toString()); }
|
|
static String str(SoxComponent c, String slot) {
|
|
if (c == null) return "";
|
|
Value v = val(c, slot);
|
|
if (v == null) return "";
|
|
if (v instanceof Buf) { Buf b = (Buf) v; return new String(b.bytes, 0, b.size).replace("\0", ""); }
|
|
String s = v.toString();
|
|
return s.endsWith(".0") ? s.substring(0, s.length() - 2) : s;
|
|
}
|
|
static Color c565(int v) {
|
|
v &= 0xFFFF;
|
|
return new Color(((v >> 11) & 31) * 255 / 31, ((v >> 5) & 63) * 255 / 63, (v & 31) * 255 / 31);
|
|
}
|
|
|
|
String status() {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int k = 4; k < LABELS.length; k++)
|
|
if (labels[k] != null && "true".equals(str(labels[k], "in"))) sb.append(str(labels[k], "trueText")).append(' ');
|
|
return sb.toString().trim();
|
|
}
|
|
|
|
String boardText() {
|
|
String[] cells = new String[16];
|
|
java.util.Arrays.fill(cells, " .");
|
|
int whites = 0;
|
|
if (v2) for (int k = 1; k <= 16; k++) {
|
|
int n = i(this.cells[k], "in");
|
|
if (n != 0) cells[k - 1] = String.format("%2d", n);
|
|
if ((i(this.cells[k], "backgroundColor") & 0xFFFF) == 0xFFFF) whites++;
|
|
}
|
|
else for (int t = 1; t <= 15; t++) {
|
|
int col = (i(tiles[t], "x") - 40) / 105, row = (i(tiles[t], "y") - 40) / 105;
|
|
if (col >= 0 && col < 4 && row >= 0 && row < 4) cells[row * 4 + col] = String.format("%2d", t);
|
|
}
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int r = 0; r < 4; r++) sb.append(r == 0 ? "" : " | ").append(String.join(" ", java.util.Arrays.copyOfRange(cells, r * 4, r * 4 + 4)));
|
|
if (v2) sb.append(" white=").append(whites);
|
|
return sb.toString();
|
|
}
|
|
|
|
void box(Graphics2D g, SoxComponent w, String text, int fg, int bg, boolean bold, boolean fill) {
|
|
int x = i(w, "x"), y = i(w, "y"), wd = i(w, "width"), ht = i(w, "height");
|
|
if (fill) { g.setColor(c565(bg)); g.fillRect(x, y, wd, ht); }
|
|
g.setColor(c565(fg));
|
|
g.setFont(new Font("Tahoma", bold ? Font.BOLD : Font.PLAIN, Math.max(9, i(w, "fontSize") * 3 / 4)));
|
|
FontMetrics fm = g.getFontMetrics();
|
|
g.drawString(text, x + (wd - fm.stringWidth(text)) / 2, y + (ht + fm.getAscent() - fm.getDescent()) / 2);
|
|
}
|
|
|
|
@Override protected void paintComponent(Graphics g0) {
|
|
Graphics2D g = (Graphics2D) g0;
|
|
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
g.setColor(c565(i(page, "backgroundColor")));
|
|
g.fillRect(0, 0, 800, 480);
|
|
g.setColor(c565(0x31A6));
|
|
g.fillRect(35, 35, 425, 425);
|
|
if (v2) for (int k = 1; k <= 16; k++) {
|
|
SoxComponent w = cells[k];
|
|
String[] opts = str(w, "text").split(";");
|
|
int n = i(w, "in");
|
|
box(g, w, n < opts.length ? opts[n].trim() : "", i(w, "textColor"), i(w, "backgroundColor"), true, true);
|
|
}
|
|
else for (int t = 1; t <= 15; t++) {
|
|
SoxComponent w = tiles[t];
|
|
box(g, w, str(w, "text"), i(w, "textColor"), i(w, "backgroundColor"), true, true);
|
|
}
|
|
for (int k = 0; k < LABELS.length; k++) {
|
|
SoxComponent w = labels[k];
|
|
if (w == null) continue;
|
|
String type = w.type.name;
|
|
if (type.equals("BooleanLabel")) {
|
|
boolean on = "true".equals(str(w, "in"));
|
|
box(g, w, str(w, on ? "trueText" : "falseText"), i(w, on ? "trueTextColor" : "falseTextColor"),
|
|
i(w, on ? "trueBackgroundColor" : "falseBackgroundColor"), true, true);
|
|
} else if (type.equals("NumericLabel")) {
|
|
String fmt = str(w, "text").replace("%.0f", str(w, "in"));
|
|
box(g, w, fmt, i(w, "textColor"), i(w, "backgroundColor"), false, true);
|
|
} else {
|
|
box(g, w, str(w, "text"), i(w, "textColor"), i(w, "backgroundColor"), i(w, "fontAttributes") == 1, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void main(String[] a) throws Exception {
|
|
String host = a.length > 0 ? a[0] : "127.0.0.1";
|
|
int port = a.length > 1 ? Integer.parseInt(a[1]) : 1876;
|
|
String user = a.length > 2 ? a[2] : "guest";
|
|
String pass = a.length > 3 ? a[3] : "";
|
|
DaspSocket sock = DaspSocket.open(-1, null, DaspSocket.SESSION_QUEUING);
|
|
OxViewer v = null;
|
|
while (v == null) { // wait for the simulator to come up
|
|
try {
|
|
SoxClient client = new SoxClient(sock, InetAddress.getByName(host), port, user, pass);
|
|
client.connect();
|
|
v = new OxViewer(client);
|
|
} catch (Exception e) {
|
|
System.out.println("waiting for SOX " + host + ":" + port + " (" + e + ")");
|
|
Thread.sleep(500);
|
|
}
|
|
}
|
|
if (GraphicsEnvironment.isHeadless()) { // -Djava.awt.headless=true: console log only
|
|
while (true) { v.poll(); Thread.sleep(200); }
|
|
}
|
|
JFrame f = new JFrame("Orion Screen - aspirin (live via SOX " + host + ":" + port + ")");
|
|
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
|
f.add(v);
|
|
f.pack();
|
|
f.setLocation(60, 60);
|
|
f.setVisible(true);
|
|
while (true) {
|
|
v.poll();
|
|
v.repaint();
|
|
Thread.sleep(200);
|
|
}
|
|
}
|
|
}
|