summaryrefslogtreecommitdiffstatshomepage
path: root/src/devices
diff options
context:
space:
mode:
author hap <happppp@users.noreply.github.com>2026-04-12 23:00:45 +0200
committer hap <happppp@users.noreply.github.com>2026-04-12 23:01:02 +0200
commit149871cb7097679ee67236324ecca65631fd82ae (patch)
tree16ce62fb1f14a051d311bdf79935fc2477016f1f /src/devices
parent0f5872aa2eeb46ae87c6a715946cce1704749d84 (diff)
grandmas/emirage: move chessboard to a device
Diffstat (limited to 'src/devices')
-rw-r--r--src/devices/machine/gmboard.cpp356
-rw-r--r--src/devices/machine/gmboard.h86
-rw-r--r--src/devices/machine/smartboard.cpp7
3 files changed, 445 insertions, 4 deletions
diff --git a/src/devices/machine/gmboard.cpp b/src/devices/machine/gmboard.cpp
new file mode 100644
index 00000000000..0ba17d6bab1
--- /dev/null
+++ b/src/devices/machine/gmboard.cpp
@@ -0,0 +1,356 @@
+// license:BSD-3-Clause
+// copyright-holders:hap
+/*
+
+Milton Bradley Grand Master motorized self-moving chessboard
+
+Electronic chessboard with motors underneath, that move a magnet around for
+automatically moving chesspieces (each piece has a magnet underneath).
+
+Used in:
+- Milton Bradley Grand Master
+- Fidelity Phantom / Chesster Phantom
+- Excalibur Mirage
+- Excalibur Phantom Force
+
+Hardware notes:
+- electronic chessboard, with room on each side for captured pieces
+- X/Y plotter motors, electromagnet (optionally including a hall affect sensor)
+
+Concept/design by Milton Bradley, for use in the Grand Master. Fidelity licensed
+or bought the design, and applied it nearly unchanged to Fidelity Phantom. Years
+later, the ex chief engineer of by then defunct Fidelity used the same technology
+while working for Excalibur.
+
+TODO:
+- optionally change output finders to callbacks, currently not needed
+- sensorboard undo buffer goes out of control, probably not worth solving this issue
+
+BTANB:
+- Motors gradually drift in Mirage, causing it to place/pick up pieces off-center.
+ It recalibrates itself once in a while but it's not enough. MAME's sensorboard
+ device can't deal with it, so there's a workaround (see realign_magnet_pos).
+ The programmer anecdotally blamed it on the hardware engineer, but it's mainly
+ a software bug.
+
+*/
+
+#include "emu.h"
+#include "gmboard.h"
+
+
+DEFINE_DEVICE_TYPE(MB_GMBOARD, gmboard_device, "mb_gmboard", "Milton Bradley Grand Master chessboard")
+
+//-------------------------------------------------
+// gmboard_device - constructor
+//-------------------------------------------------
+
+gmboard_device::gmboard_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock) :
+ device_t(mconfig, MB_GMBOARD, tag, owner, clock),
+ m_board(*this, "board"),
+ m_piece_hand(*this, "cpu_hand"),
+ m_out_motor(*this, "motor%u", 0U),
+ m_out_pos(*this, "pos_%c", unsigned('x')),
+ m_quad_cb(*this)
+{
+ // (just to prevent a divide by 0 if it's not configured)
+ m_width = 1;
+ m_height = 1;
+ m_square = 1;
+ m_x_offset = 0;
+ m_y_offset = 0;
+
+ m_speed = attotime::from_msec(200);
+}
+
+
+//-------------------------------------------------
+// initialization
+//-------------------------------------------------
+
+void gmboard_device::device_start()
+{
+ // resolve outputs
+ m_piece_hand.resolve();
+ m_out_motor.resolve();
+ m_out_pos.resolve();
+
+ // zerofill
+ m_magnet = 0;
+ memset(m_pieces_map, 0, sizeof(m_pieces_map));
+ memset(m_motor_dir, 0, sizeof(m_motor_dir));
+ memset(m_motor_max, 0, sizeof(m_motor_max));
+ memset(m_motor_pos, 0, sizeof(m_motor_pos));
+ memset(m_motor_quad, 0, sizeof(m_motor_quad));
+ memset(m_motor_drift, 0, sizeof(m_motor_drift));
+
+ // register for savestates
+ save_item(NAME(m_magnet));
+ save_item(NAME(m_pieces_map));
+ save_item(NAME(m_motor_dir));
+ save_item(NAME(m_motor_pos));
+ save_item(NAME(m_motor_quad));
+ save_item(NAME(m_motor_remain));
+ save_item(NAME(m_motor_drift));
+
+ init_motors();
+}
+
+void gmboard_device::device_reset()
+{
+ memset(m_motor_drift, 0, sizeof(m_motor_drift));
+ m_magnet = 0;
+
+ output_magnet_pos();
+}
+
+void gmboard_device::init_board(u8 data)
+{
+ m_board->preset_chess(data);
+
+ // reposition pieces if board will be rotated
+ if (data & 2)
+ {
+ for (int y = 0; y < 8; y++)
+ for (int x = 7; x >= 0; x--)
+ {
+ m_board->write_piece(x + 4, y, m_board->read_piece(x, y));
+ m_board->write_piece(x, y, 0);
+ }
+ }
+}
+
+void gmboard_device::clear_board(u8 data)
+{
+ memset(m_pieces_map, 0, sizeof(m_pieces_map));
+ m_piece_hand = 0;
+ m_board->clear_board(data);
+}
+
+
+//-------------------------------------------------
+// motor sim
+//-------------------------------------------------
+
+void gmboard_device::init_motors()
+{
+ m_motor_max[0] = m_width - 1;
+ m_motor_max[1] = m_height - 1;
+
+ // start at A1
+ m_motor_pos[0] = std::min<u32>(2 * m_square + m_x_offset, m_motor_max[0]);
+ m_motor_pos[1] = std::min<u32>(7 * m_square + m_y_offset, m_motor_max[1]);
+
+ m_motor_period = m_speed / m_square;
+
+ for (int i = 0; i < 2; i++)
+ {
+ m_motor_timer[i] = timer_alloc(FUNC(gmboard_device::motor_count), this);
+ m_motor_remain[i] = m_motor_period / 2;
+ }
+}
+
+void gmboard_device::get_scaled_pos(double *x, double *y)
+{
+ // scale down to 4 counts per square
+ *x = std::clamp(double(int(m_motor_pos[0]) - m_x_offset) / (m_square / 4.0) + 2.0, 0.0, 48.0);
+ *y = std::clamp(double(int(m_motor_pos[1]) - m_y_offset) / (m_square / 4.0) + 2.0, 0.0, 32.0);
+}
+
+void gmboard_device::output_magnet_pos()
+{
+ double x, y;
+ get_scaled_pos(&x, &y);
+
+ // put active state on x bit 11
+ const int active = m_magnet ? 0x800 : 0;
+ m_out_pos[0] = int(x * 25.0 + 0.5) | active;
+ m_out_pos[1] = int(y * 25.0 + 0.5);
+ m_out_motor[4] = m_magnet;
+}
+
+void gmboard_device::realign_magnet_pos()
+{
+ // compensate for gradual drift, see BTANB
+ for (int i = 0; i < 2; )
+ {
+ double pos[2];
+ get_scaled_pos(&pos[0], &pos[1]);
+
+ const double limit = 1.0 / (m_square / 16.0);
+ int inc = 0;
+
+ if ((round(pos[i]) - pos[i]) > limit && m_motor_pos[i] < m_motor_max[i] - 4)
+ inc = 1;
+ else if ((round(pos[i]) - pos[i]) < -limit && m_motor_pos[i] > 4)
+ inc = -1;
+ else
+ i++;
+
+ if (inc != 0)
+ {
+ m_motor_pos[i] += inc * 4;
+ m_motor_drift[i] -= inc;
+
+ logerror("motor %c drift error (%d total)\n", 'X' + i, m_motor_drift[i]);
+ }
+ }
+}
+
+void gmboard_device::magnet_w(int state)
+{
+ state = state ? 1 : 0;
+
+ if (state == m_magnet)
+ return;
+ m_magnet = state;
+
+ if (m_piece_hand != 0)
+ realign_magnet_pos();
+ output_magnet_pos();
+
+ double dx, dy;
+ get_scaled_pos(&dx, &dy);
+
+ int mx = dx + 0.5;
+ int my = dy + 0.5;
+
+ // convert motors position into board coordinates
+ int x = mx / 4 - 2;
+ int y = 7 - (my / 4);
+
+ if (x < 0)
+ x += 12;
+
+ const bool valid_pos = (mx & 3) == 2 && (my & 3) == 2;
+
+ // sensorboard handling is almost the same as fidelity/phantom.cpp
+ if (state)
+ {
+ if (valid_pos)
+ {
+ // pick up piece, unless it was picked up by the user
+ const int pos = (y << 4 & 0xf0) | (x & 0x0f);
+ if (pos != m_board->get_handpos())
+ {
+ m_piece_hand = m_board->read_piece(x, y);
+
+ if (m_piece_hand != 0)
+ {
+ m_board->write_piece(x, y, 0);
+ m_board->refresh();
+ }
+ }
+ }
+ else
+ {
+ // pick up piece from internal pieces map
+ m_piece_hand = m_pieces_map[my][mx];
+ m_pieces_map[my][mx] = 0;
+ }
+ }
+ else if (m_piece_hand != 0)
+ {
+ if (valid_pos)
+ {
+ // collision with piece on board (user interference)
+ if (m_board->read_piece(x, y) != 0)
+ popmessage("Collision at %c%d!", x + 'A', y + 1);
+ else
+ {
+ m_board->write_piece(x, y, m_piece_hand);
+ m_board->refresh();
+ }
+ }
+ else
+ {
+ // collision with internal pieces map (shouldn't happen)
+ if (m_pieces_map[my][mx] != 0)
+ popmessage("Internal collision!");
+ else
+ m_pieces_map[my][mx] = m_piece_hand;
+ }
+
+ m_piece_hand = 0;
+ }
+}
+
+TIMER_CALLBACK_MEMBER(gmboard_device::motor_count)
+{
+ const int m = param ? 1 : 0;
+ assert(m_motor_dir[m] & 3);
+
+ // 1 quarter rotation per period
+ int inc = 0;
+ if (m_motor_dir[m] & 2)
+ {
+ if (m_motor_pos[m] < m_motor_max[m])
+ inc = 1;
+ }
+ else if (m_motor_pos[m] > 0)
+ inc = -1;
+
+ m_motor_remain[m] = m_motor_period;
+
+ if (inc == 0)
+ return;
+
+ m_motor_pos[m] += inc;
+ m_motor_timer[m]->adjust(m_motor_period, m);
+
+ output_magnet_pos();
+
+ // update quadrature encoder
+ static const u8 lut_quad[4] = { 0,1,3,2 };
+ m_motor_quad[m] = lut_quad[m_motor_pos[m] & 3];
+
+ m_quad_cb[m](m_motor_quad[m]);
+}
+
+void gmboard_device::motor_w(offs_t offset, u8 data)
+{
+ const int m = offset & 1;
+ data &= 3;
+
+ for (int i = 0; i < 2; i++)
+ m_out_motor[offset * 2 + i] = BIT(data, i);
+
+ // it's not moving when both directions are set
+ if (data == 3)
+ data = 0;
+
+ if (data == m_motor_dir[m])
+ return;
+
+ // remember remaining time
+ if (m_motor_dir[m] & 3 && !m_motor_timer[m]->remaining().is_never())
+ {
+ m_motor_remain[m] = m_motor_timer[m]->remaining();
+ m_motor_timer[m]->adjust(attotime::never);
+ }
+
+ // invert remaining time if direction flipped
+ if ((m_motor_dir[m] ^ data) & 1)
+ m_motor_remain[m] = m_motor_period - m_motor_remain[m];
+
+ m_motor_dir[m] = data;
+
+ // (re)start the timer
+ if (data & 3)
+ m_motor_timer[m]->adjust(m_motor_remain[m], m);
+}
+
+
+//-------------------------------------------------
+// device_add_mconfig - add device-specific
+// machine configuration
+//-------------------------------------------------
+
+void gmboard_device::device_add_mconfig(machine_config &config)
+{
+ SENSORBOARD(config, m_board).set_type(sensorboard_device::BUTTONS);
+ m_board->set_size(8+4, 8);
+ m_board->clear_cb().set(FUNC(gmboard_device::clear_board));
+ m_board->init_cb().set(FUNC(gmboard_device::init_board));
+ m_board->set_delay(attotime::from_msec(150));
+}
diff --git a/src/devices/machine/gmboard.h b/src/devices/machine/gmboard.h
new file mode 100644
index 00000000000..dd5fa59156c
--- /dev/null
+++ b/src/devices/machine/gmboard.h
@@ -0,0 +1,86 @@
+// license:BSD-3-Clause
+// copyright-holders:hap
+/*
+
+ Milton Bradley Grand Master motorized self-moving chessboard
+
+*/
+
+#ifndef MAME_MACHINE_GMBOARD_H
+#define MAME_MACHINE_GMBOARD_H
+
+#pragma once
+
+#include "machine/sensorboard.h"
+
+class gmboard_device : public device_t
+{
+public:
+ gmboard_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock = 0);
+
+ // configuration helpers
+ template <std::size_t N> auto quad_cb() { return m_quad_cb[N].bind(); } // x/y motor quadrature encoder state
+ gmboard_device &set_size(u16 width, u16 height, u16 square) { m_width = width; m_height = height; m_square = square; return *this; }
+ gmboard_device &set_offsets(u16 x_offset, u16 y_offset) { m_x_offset = x_offset; m_y_offset = y_offset; return *this; }
+ gmboard_device &set_speed(attotime speed) { m_speed = speed; return *this; }
+
+ sensorboard_device &get() { return *m_board; }
+
+ // external read/write handlers
+ int magnet_r() { return (started() && m_piece_hand) ? 1 : 0; }
+ void magnet_w(int state);
+ void motor_w(offs_t offset, u8 data);
+ u8 quad_r(offs_t offset) { return m_motor_quad[offset & 1]; }
+
+ u16 read_file(u8 x, bool reverse = false) { return m_board->read_file(x, reverse); }
+ u16 read_rank(u8 y, bool reverse = false) { return m_board->read_rank(y, reverse); }
+
+protected:
+ // device-level overrides
+ virtual void device_start() override ATTR_COLD;
+ virtual void device_reset() override ATTR_COLD;
+ virtual void device_add_mconfig(machine_config &config) override ATTR_COLD;
+
+private:
+ required_device<sensorboard_device> m_board;
+ output_finder<> m_piece_hand;
+ output_finder<5> m_out_motor;
+ output_finder<2> m_out_pos;
+
+ u8 m_magnet;
+ u8 m_pieces_map[0x40][0x40];
+
+ u8 m_motor_dir[2];
+ u32 m_motor_max[2];
+ u32 m_motor_pos[2];
+ u8 m_motor_quad[2];
+ s32 m_motor_drift[2]; // diagnostics
+
+ attotime m_motor_period;
+ attotime m_motor_remain[2];
+ emu_timer *m_motor_timer[2];
+
+ u16 m_width; // motor range
+ u16 m_height; // "
+ u16 m_square; // number of quarter rotations per square
+ u16 m_x_offset; // unscaled offset relative to the bottom-left corner
+ u16 m_y_offset; // "
+ attotime m_speed; // time per square at full speed
+
+ devcb_write8::array<2> m_quad_cb;
+
+ void init_board(u8 data);
+ void clear_board(u8 data);
+ void init_motors();
+
+ void get_scaled_pos(double *x, double *y);
+ void output_magnet_pos();
+ void realign_magnet_pos();
+
+ TIMER_CALLBACK_MEMBER(motor_count);
+};
+
+
+DECLARE_DEVICE_TYPE(MB_GMBOARD, gmboard_device)
+
+#endif // MAME_MACHINE_GMBOARD_H
diff --git a/src/devices/machine/smartboard.cpp b/src/devices/machine/smartboard.cpp
index d699b1306df..22a73032075 100644
--- a/src/devices/machine/smartboard.cpp
+++ b/src/devices/machine/smartboard.cpp
@@ -1,6 +1,6 @@
// license:BSD-3-Clause
// copyright-holders:Sandro Ronco, hap
-/******************************************************************************
+/*
Tasc SmartBoard SB30 (analog)
@@ -18,7 +18,7 @@ SB20 and the newer SB30 are not emulated. They're on different hardware, with
embedded CPU to reduce I/O overhead. Note, those are not compatible with old
versions of Tasc R30.
-******************************************************************************/
+*/
#include "emu.h"
#include "smartboard.h"
@@ -128,8 +128,7 @@ ioport_constructor tasc_sb30_device::device_input_ports() const
void tasc_sb30_device::device_add_mconfig(machine_config &config)
{
- SENSORBOARD(config, m_board);
- m_board->set_type(sensorboard_device::INDUCTIVE);
+ SENSORBOARD(config, m_board).set_type(sensorboard_device::INDUCTIVE);
m_board->set_max_id(32);
m_board->init_cb().set(FUNC(tasc_sb30_device::init_cb));
m_board->spawn_cb().set(FUNC(tasc_sb30_device::spawn_cb));