578 lines
16 KiB
C
578 lines
16 KiB
C
//
|
|
// Author: Jeremy Rand
|
|
// Date: July 20, 2012
|
|
//
|
|
// This is the implementation for the Curta emulator UI.
|
|
//
|
|
|
|
|
|
#include <stdio.h>
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
#include <conio.h>
|
|
#include <apple2.h>
|
|
#include <joystick.h>
|
|
#include <tgi.h>
|
|
|
|
#include "curtaModel.h"
|
|
#include "joystick.h"
|
|
#include "curtaUI.h"
|
|
#include "drivers/a2_hires_drv.h"
|
|
|
|
|
|
typedef int8_t tAction;
|
|
|
|
#define ACTION_OPERAND_SHIFT_LEFT 0
|
|
#define ACTION_OPERAND_SHIFT_RIGHT 1
|
|
#define ACTION_OPERAND_INC 2
|
|
#define ACTION_OPERAND_DEC 3
|
|
#define ACTION_RESULT_SHIFT_LEFT 4
|
|
#define ACTION_RESULT_SHIFT_RIGHT 5
|
|
#define ACTION_ADD 6
|
|
#define ACTION_SUBTRACT 7
|
|
#define ACTION_CLEAR 8
|
|
#define ACTION_NULL 9
|
|
|
|
|
|
#define OPERAND_COLOR TGI_COLOR_WHITE
|
|
#define OPERAND_OFFSET 12
|
|
#define SLIDER_BAR_COLOR TGI_COLOR_BLUE
|
|
#define SELECTED_SLIDER_BAR_COLOR TGI_COLOR_VIOLET
|
|
#define SLIDER_COLOR TGI_COLOR_ORANGE
|
|
#define SELECTED_SLIDER_COLOR TGI_COLOR_BLACK
|
|
#define SLIDER_X_BORDER 8
|
|
#define SLIDER_Y_BORDER 15
|
|
#define SLIDER_BAR_WIDTH 12
|
|
#define SLIDER_BAR_HEIGHT 130
|
|
#define SLIDER_BAR_SPACING 20
|
|
|
|
#define SLIDER_X_OFFSET 2
|
|
#define SLIDER_Y_OFFSET 1
|
|
#define SLIDER_WIDTH 8
|
|
#define SLIDER_HEIGHT 11
|
|
#define SLIDER_Y_SPACING (SLIDER_HEIGHT + (2 * SLIDER_Y_OFFSET))
|
|
|
|
|
|
// Bad news here. This code in this module uses the defines for the
|
|
// number of digits in counter and result except for this display
|
|
// buffer and the offsets into it. The code assumes a specific size.
|
|
// It doesn't really have to. I could build the template string at
|
|
// runtime and even statically allocate the buffer based on a formula
|
|
// of the sizes, but that just seemed too much work. Fix it if you
|
|
// want to change the width of these fields
|
|
static char displayBuffer[] =
|
|
"\n\n"
|
|
"Counter: 0 0 0 0 0 0 0 0\n"
|
|
" Result: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
|
|
" ^";
|
|
|
|
#define COUNTER_OFFSET 25
|
|
#define RESULT_OFFSET 50
|
|
#define BASE_OFFSET 103
|
|
|
|
|
|
static void playSound(int8_t freq, int8_t duration)
|
|
{
|
|
while (duration > 0) {
|
|
asm ("STA %w", 0xc030);
|
|
while (freq > 0) {
|
|
freq--;
|
|
}
|
|
duration--;
|
|
}
|
|
}
|
|
|
|
|
|
static void updateCounter(void)
|
|
{
|
|
tDigitPos pos;
|
|
char *ptr = &(displayBuffer[COUNTER_OFFSET]);
|
|
|
|
for(pos = NUM_COUNTER_DIGITS - 1; pos >= 0; pos--) {
|
|
*ptr = GET_COUNTER_DIGIT(pos) + '0';
|
|
ptr+=2;
|
|
}
|
|
}
|
|
|
|
|
|
static void updateResult(void)
|
|
{
|
|
tDigitPos pos;
|
|
char *ptr = &(displayBuffer[RESULT_OFFSET]);
|
|
|
|
for(pos = NUM_RESULT_DIGITS - 1; pos >= 0; pos--) {
|
|
*ptr = GET_RESULT_DIGIT(pos) + '0';
|
|
ptr+=2;
|
|
}
|
|
|
|
ptr = &(displayBuffer[BASE_OFFSET]);
|
|
for(pos = BASE_POS_MAX - 1; pos >= BASE_POS_MIN; pos--) {
|
|
if (pos == basePos) {
|
|
*ptr = '^';
|
|
} else {
|
|
*ptr = ' ';
|
|
}
|
|
ptr+=2;
|
|
}
|
|
}
|
|
|
|
|
|
static void printState(void)
|
|
{
|
|
updateCounter();
|
|
updateResult();
|
|
puts(displayBuffer);
|
|
}
|
|
|
|
|
|
static void drawSlider(char xPos, tDigit digit, char color)
|
|
{
|
|
tgi_setcolor(color);
|
|
tgi_bar(xPos + SLIDER_X_OFFSET,
|
|
SLIDER_Y_BORDER + SLIDER_Y_OFFSET + (SLIDER_Y_SPACING * digit),
|
|
xPos + SLIDER_X_OFFSET + SLIDER_WIDTH,
|
|
SLIDER_Y_BORDER + SLIDER_Y_OFFSET + (SLIDER_Y_SPACING * digit) + SLIDER_HEIGHT);
|
|
}
|
|
|
|
|
|
static void drawBar(char xPos, tDigit digit, bool isSelected)
|
|
{
|
|
char barColor = SLIDER_BAR_COLOR;
|
|
char sliderColor = SLIDER_COLOR;
|
|
|
|
if (isSelected) {
|
|
barColor = SELECTED_SLIDER_BAR_COLOR;
|
|
sliderColor = SELECTED_SLIDER_COLOR;
|
|
}
|
|
|
|
// Draw slider bar
|
|
tgi_setcolor(barColor);
|
|
tgi_bar(xPos, SLIDER_Y_BORDER, xPos + SLIDER_BAR_WIDTH, SLIDER_Y_BORDER + SLIDER_BAR_HEIGHT);
|
|
|
|
// Draw slider
|
|
drawSlider(xPos, digit, sliderColor);
|
|
}
|
|
|
|
|
|
static void drawText(char xPos, tDigit digit)
|
|
{
|
|
char buffer[2];
|
|
|
|
// Clear old text
|
|
tgi_setcolor(TGI_COLOR_BLACK);
|
|
tgi_bar(xPos, 0, xPos + SLIDER_BAR_WIDTH, SLIDER_Y_BORDER - 1);
|
|
|
|
// Draw text label
|
|
buffer[0] = digit + '0';
|
|
buffer[1] = '\0';
|
|
tgi_setcolor(OPERAND_COLOR);
|
|
tgi_outtextxy(xPos + OPERAND_OFFSET, 0, buffer);
|
|
}
|
|
|
|
|
|
static void changeOperand(tDigitPos pos, tDigit oldValue, tDigit newValue)
|
|
{
|
|
char xPos;
|
|
char barColor = SELECTED_SLIDER_BAR_COLOR;
|
|
char sliderColor = SELECTED_SLIDER_COLOR;
|
|
|
|
if (!IS_VALID_OPERAND_POS(pos))
|
|
return;
|
|
|
|
xPos = SLIDER_X_BORDER + (SLIDER_BAR_SPACING * (NUM_OPERAND_DIGITS - pos - 1));
|
|
|
|
playSound(100, 10);
|
|
drawText(xPos, newValue);
|
|
drawSlider(xPos, oldValue, SELECTED_SLIDER_BAR_COLOR);
|
|
drawSlider(xPos, newValue, SELECTED_SLIDER_COLOR);
|
|
}
|
|
|
|
|
|
static void changeSelectedOperand(tDigitPos pos)
|
|
{
|
|
char xPos;
|
|
tDigit digit;
|
|
|
|
if (!IS_VALID_OPERAND_POS(pos))
|
|
return;
|
|
|
|
digit = GET_OPERAND_DIGIT(pos);
|
|
xPos = SLIDER_X_BORDER + (SLIDER_BAR_SPACING * (NUM_OPERAND_DIGITS - pos - 1));
|
|
|
|
drawBar(xPos, digit, IS_SELECTED_OPERAND(pos));
|
|
}
|
|
|
|
|
|
static void drawOperand(tDigitPos pos)
|
|
{
|
|
char xPos;
|
|
tDigit digit;
|
|
|
|
if (!IS_VALID_OPERAND_POS(pos))
|
|
return;
|
|
|
|
digit = GET_OPERAND_DIGIT(pos);
|
|
xPos = SLIDER_X_BORDER + (SLIDER_BAR_SPACING * (NUM_OPERAND_DIGITS - pos - 1));
|
|
|
|
drawText(xPos, digit);
|
|
drawBar(xPos, digit, IS_SELECTED_OPERAND(pos));
|
|
}
|
|
|
|
|
|
static tAction getNextAction(void)
|
|
{
|
|
static bool firstCall = true;
|
|
static tJoyPos oldJoyPos;
|
|
static unsigned int possibleActions = 0xffff;
|
|
|
|
tJoyState joyState;
|
|
tAction result = ACTION_NULL;
|
|
|
|
if (firstCall) {
|
|
oldJoyPos = JOY_POS_CENTER;
|
|
firstCall = false;
|
|
return result;
|
|
}
|
|
|
|
getJoystickState(&joyState);
|
|
|
|
if ((joyState.position != JOY_POS_CENTER) &&
|
|
(joyState.position != JOY_POS_DOWN)) {
|
|
if (possibleActions == (1 << ACTION_ADD)) {
|
|
playSound(400, 10);
|
|
}
|
|
if (possibleActions == (1 << ACTION_SUBTRACT)) {
|
|
playSound(100, 10);
|
|
}
|
|
}
|
|
|
|
if (joyState.position == oldJoyPos) {
|
|
return result;
|
|
} else if (oldJoyPos == JOY_POS_CENTER) {
|
|
if (joyState.position == JOY_POS_LEFT) {
|
|
if (joyState.button0) {
|
|
possibleActions = (1 << ACTION_RESULT_SHIFT_LEFT);
|
|
} else {
|
|
possibleActions = (1 << ACTION_OPERAND_SHIFT_LEFT);
|
|
}
|
|
} else if (joyState.position == JOY_POS_RIGHT) {
|
|
if (joyState.button0) {
|
|
possibleActions = (1 << ACTION_RESULT_SHIFT_RIGHT);
|
|
} else {
|
|
possibleActions = (1 << ACTION_OPERAND_SHIFT_RIGHT);
|
|
}
|
|
} else if (joyState.position == JOY_POS_UP) {
|
|
possibleActions = (1 << ACTION_OPERAND_DEC);
|
|
} else if (joyState.position == JOY_POS_DOWN) {
|
|
possibleActions = (1 << ACTION_OPERAND_INC);
|
|
if (joyState.button0) {
|
|
possibleActions |= (1 << ACTION_SUBTRACT);
|
|
} else if (joyState.button1) {
|
|
possibleActions |= (1 << ACTION_CLEAR);
|
|
} else {
|
|
possibleActions |= (1 << ACTION_ADD);
|
|
}
|
|
}
|
|
} else {
|
|
if (possibleActions & (1 << ACTION_OPERAND_SHIFT_LEFT)) {
|
|
if (joyState.position == JOY_POS_CENTER) {
|
|
result = ACTION_OPERAND_SHIFT_LEFT;
|
|
} else {
|
|
possibleActions &= (~(1 << ACTION_OPERAND_SHIFT_LEFT));
|
|
}
|
|
}
|
|
|
|
if (possibleActions & (1 << ACTION_OPERAND_SHIFT_RIGHT)) {
|
|
if (joyState.position == JOY_POS_CENTER) {
|
|
result = ACTION_OPERAND_SHIFT_RIGHT;
|
|
} else {
|
|
possibleActions &= (~(1 << ACTION_OPERAND_SHIFT_RIGHT));
|
|
}
|
|
}
|
|
|
|
if (possibleActions & (1 << ACTION_OPERAND_INC)) {
|
|
if (joyState.position == JOY_POS_CENTER) {
|
|
result = ACTION_OPERAND_INC;
|
|
} else {
|
|
possibleActions &= (~(1 << ACTION_OPERAND_INC));
|
|
}
|
|
}
|
|
|
|
if (possibleActions & (1 << ACTION_OPERAND_DEC)) {
|
|
if (joyState.position == JOY_POS_CENTER) {
|
|
result = ACTION_OPERAND_DEC;
|
|
} else {
|
|
possibleActions &= (~(1 << ACTION_OPERAND_DEC));
|
|
}
|
|
}
|
|
|
|
if (possibleActions & (1 << ACTION_RESULT_SHIFT_LEFT)) {
|
|
if (joyState.position == JOY_POS_CENTER) {
|
|
result = ACTION_RESULT_SHIFT_LEFT;
|
|
} else {
|
|
possibleActions &= (~(1 << ACTION_RESULT_SHIFT_LEFT));
|
|
}
|
|
}
|
|
|
|
if (possibleActions & (1 << ACTION_RESULT_SHIFT_RIGHT)) {
|
|
if (joyState.position == JOY_POS_CENTER) {
|
|
result = ACTION_RESULT_SHIFT_RIGHT;
|
|
} else {
|
|
possibleActions &= (~(1 << ACTION_RESULT_SHIFT_RIGHT));
|
|
}
|
|
}
|
|
|
|
if (possibleActions & (1 << ACTION_ADD)) {
|
|
if ((joyState.position == JOY_POS_DOWN) &&
|
|
(oldJoyPos == JOY_POS_DOWN_RIGHT)) {
|
|
result = ACTION_ADD;
|
|
possibleActions = 0;
|
|
} else if (joyState.position != oldJoyPos + 1) {
|
|
possibleActions &= (~(1 << ACTION_ADD));
|
|
}
|
|
} else if (possibleActions & (1 << ACTION_SUBTRACT)) {
|
|
if ((joyState.position == JOY_POS_DOWN) &&
|
|
(oldJoyPos == JOY_POS_DOWN_RIGHT)) {
|
|
result = ACTION_SUBTRACT;
|
|
possibleActions = 0;
|
|
} else if (joyState.position != oldJoyPos + 1) {
|
|
possibleActions &= (~(1 << ACTION_SUBTRACT));
|
|
}
|
|
} else if (possibleActions & (1 << ACTION_CLEAR)) {
|
|
if ((joyState.position == JOY_POS_DOWN) &&
|
|
(oldJoyPos == JOY_POS_DOWN_RIGHT)) {
|
|
result = ACTION_CLEAR;
|
|
possibleActions = 0;
|
|
} else if (joyState.position != oldJoyPos + 1) {
|
|
possibleActions &= (~(1 << ACTION_CLEAR));
|
|
} else if ((joyState.position == JOY_POS_UP_LEFT) ||
|
|
(joyState.position == JOY_POS_DOWN_RIGHT)) {
|
|
playSound(100, 50);
|
|
}
|
|
}
|
|
|
|
if (joyState.position == JOY_POS_CENTER) {
|
|
possibleActions = -1;
|
|
}
|
|
}
|
|
|
|
oldJoyPos = joyState.position;
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
void textMode(void)
|
|
{
|
|
clrscr();
|
|
asm ("STA %w", 0xc051);
|
|
}
|
|
|
|
|
|
void graphicsMode(void)
|
|
{
|
|
asm ("STA %w", 0xc050);
|
|
}
|
|
|
|
|
|
void printInstructions(void)
|
|
{
|
|
textMode();
|
|
|
|
// 0000000001111111111222222222233333333334
|
|
// 1234567890123456789012345678901234567890
|
|
printf(" CURTASIM\n"
|
|
"\n"
|
|
"A CURTA IS A MECHANICAL CALCULATOR THAT\n"
|
|
"LOOKS SOMEWHAT LIKE A PEPPER GRINDER.\n"
|
|
"GOOGLE CURTA AND CHECKOUT SOME PICS.\n"
|
|
"TO REPLACE THE CRANK, THIS SIMULATION\n"
|
|
"USES A JOYSTICK.\n"
|
|
"\n"
|
|
"THE PRIMARY INTERACTION IS PERFORMED\n"
|
|
"BY \"CRANKING\" THE JOYSTICK THROUGH\n"
|
|
"360 DEGREES. TO PERFORM A CRANK, START\n"
|
|
"WITH THE JOYSTICK CENTERED. PULL THE\n"
|
|
"JOYSTICK TOWARDS YOU AND THEN CRANK\n"
|
|
"AROUND A CIRCLE CLOCKWISE UNTIL IT IS\n"
|
|
"AGAIN PULLED TOWARDS YOU. THEN RELEASE\n"
|
|
"TO THE CENTRE POSITION.\n"
|
|
"\n"
|
|
"THE OPERAND IS THE SET OF DIGITS AT THE\n"
|
|
"TOP OF THE SCREEN. THE SLIDERS CONTROL\n"
|
|
"THE OPERAND. THE RESULT IS AT THE\n"
|
|
"BOTTOM OF THE SCREEN. THE COUNTER IS A\n"
|
|
"MULTIPLICAND.\n"
|
|
"\n"
|
|
"PRESS ANY KEY FOR MORE INFORMATION");
|
|
cgetc();
|
|
|
|
clrscr();
|
|
|
|
// 0000000001111111111222222222233333333334
|
|
// 1234567890123456789012345678901234567890
|
|
printf(" CURTASIM\n"
|
|
"\n"
|
|
"BELOW THE RESULT IS A CARAT THAT POINTS\n"
|
|
"TO A DIGIT. THIS IS THE SAME AS THE\n"
|
|
"CARRIAGE POSITION ON A REAL CURTA.\n"
|
|
"\n"
|
|
"THE JOYSTICK OPERATIONS ARE:\n"
|
|
"\n"
|
|
" LEFT/RIGHT - SELECT A DIGIT IN THE\n"
|
|
" OPERAND\n"
|
|
" UP/DOWN - CHANGE THE SELECTED DIGIT IN\n"
|
|
" THE OPERAND\n"
|
|
" LEFT/RIGHT WITH BUTTON 0 - CHANGE\n"
|
|
" THE CARRIAGE POSITION\n"
|
|
" CRANK - ADD OPERAND TIMES CARRIAGE\n"
|
|
" POSITION TO THE RESULT\n"
|
|
" CRANK WITH BUTTON 0 - SUBTRACT OPERAND\n"
|
|
" TIMES CARRIAGE POSITION FROM\n"
|
|
" THE RESULT\n"
|
|
" CRANK WITH BUTTON 1 - CLEAR THE RESULT\n"
|
|
" AND COUNTER\n"
|
|
"\n"
|
|
"PRESS ANY KEY FOR MORE INFORMATION");
|
|
|
|
cgetc();
|
|
|
|
clrscr();
|
|
// 0000000001111111111222222222233333333334
|
|
// 1234567890123456789012345678901234567890
|
|
printf(" CURTASIM\n"
|
|
"\n"
|
|
"THESE KEYBOARD COMMANDS CAN BE USED:\n"
|
|
"\n"
|
|
" Q - QUIT THE SIMULATION\n"
|
|
" H - PRINT THIS HELP AGAIN\n"
|
|
"\n"
|
|
"IMAGINE YOU WANT TO MULTIPLY 123 BY 990."
|
|
"PERFORM A CLEAR OPERATION IF RESULT IS\n"
|
|
"NOT ZERO. USE LEFT/RIGHT/UP/DOWN\n"
|
|
"OPERATIONS TO PUT 123 IN THE OPERAND.\n"
|
|
"HOLD BUTTON 0 AND PERFORM LEFT MOVES TO\n"
|
|
"GET THE CARRIAGE POINTING TO THE\n"
|
|
"THOUSANDS POSITION IN THE RESULT.\n"
|
|
"\n"
|
|
"NOW DO AN ADD OPERATION. THIS FINDS\n"
|
|
"123 * 1000 = 123000 AND ADDS IT TO THE\n"
|
|
"RESULT. MOVE THE CARRIAGE TO THE TENS\n"
|
|
"POSITION. DO A SUBTRACT WHICH SUBTRACTS"
|
|
"123 * 10 = 1230 FROM RESULT. COUNT SAYS"
|
|
"990 AND RESULT HAS THE ANSWER: 121770\n"
|
|
"\n"
|
|
"PRESS ANY KEY TO START THE SIMULATION");
|
|
|
|
cgetc();
|
|
}
|
|
|
|
|
|
void redrawUI(void)
|
|
{
|
|
tDigitPos pos;
|
|
|
|
graphicsMode();
|
|
tgi_clear();
|
|
for (pos = 0; pos < NUM_OPERAND_DIGITS; pos++) {
|
|
drawOperand(pos);
|
|
}
|
|
|
|
// Mixed text and graphics mode
|
|
asm ("STA %w", 0xc053);
|
|
printf("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
|
|
printState();
|
|
}
|
|
|
|
|
|
void initUI(void)
|
|
{
|
|
// Install drivers
|
|
initDevice(changeOperand, changeSelectedOperand);
|
|
|
|
tgi_install(&a2_hires_drv);
|
|
|
|
tgi_init();
|
|
redrawUI();
|
|
}
|
|
|
|
|
|
bool processNextEvent(void)
|
|
{
|
|
bool timeToQuit = false;
|
|
|
|
// Exit on ESC
|
|
if (kbhit()) {
|
|
switch (cgetc()) {
|
|
case 27:
|
|
case 'Q':
|
|
case 'q':
|
|
timeToQuit = true;
|
|
break;
|
|
|
|
case 'H':
|
|
case 'h':
|
|
printInstructions();
|
|
redrawUI();
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (getNextAction()) {
|
|
case ACTION_NULL:
|
|
break;
|
|
case ACTION_OPERAND_SHIFT_LEFT:
|
|
shiftOperandPos(true);
|
|
break;
|
|
|
|
case ACTION_OPERAND_SHIFT_RIGHT:
|
|
shiftOperandPos(false);
|
|
break;
|
|
|
|
case ACTION_OPERAND_INC:
|
|
incOperandPos(selectedOperand);
|
|
break;
|
|
|
|
case ACTION_OPERAND_DEC:
|
|
decOperandPos(selectedOperand);
|
|
break;
|
|
|
|
case ACTION_RESULT_SHIFT_LEFT:
|
|
shiftResultPos(true);
|
|
playSound(100,10);
|
|
printState();
|
|
playSound(200,20);
|
|
break;
|
|
|
|
case ACTION_RESULT_SHIFT_RIGHT:
|
|
shiftResultPos(false);
|
|
playSound(100,10);
|
|
printState();
|
|
playSound(200,20);
|
|
break;
|
|
|
|
case ACTION_ADD:
|
|
crank(false);
|
|
printState();
|
|
break;
|
|
|
|
case ACTION_SUBTRACT:
|
|
crank(true);
|
|
printState();
|
|
break;
|
|
|
|
case ACTION_CLEAR:
|
|
clearDevice();
|
|
printState();
|
|
break;
|
|
}
|
|
return timeToQuit;
|
|
}
|
|
|
|
|
|
void shutdownUI(void)
|
|
{
|
|
// Uninstall drivers
|
|
tgi_uninstall();
|
|
}
|