Ported the program to the Apple II.

Implementation notes:
- General display uses the Apple II hires mode accessed via custom asm functions.
- Menu display uses the 4 line bottom text option of the Apple II hires mode via cc65 CONIO functions.
- All hires access is byte aligned, therefore the horizontal resolution is 40 (bytes).
- Hires access is done via simple (binary) ROPs (raster operations) by using actual 6502 (immediate) opcodes.

The C64 and Curses implementation both make heavy use of colors while Apple II implementation mustn't depend on (but may benefit from) colors. Therefore the user-operated cursor inverts the border of the current field. It's hard to find a compromise between making the cursor visible well and showing the piece "under" the cursor well. Additionally it is desirable to show different cursor states (empty, invalid, valid). The approach chosen is to have different thicknesses of the inverted border:
- Valid: Thin
- Invalid: Medium
- Empty: Thick

When it comes to showing attackers/defenders (via the keys 'a'/'d') there's no alternative to resorting to colors:
- Attackers: Red
- Defenders: Green

So the only field display variant left is the piece selected for moving. Instead of introducing a third type of highlighting (beside border inversion and coloring) it is simply colored Magenta. The reasoning:
- On a monochrome display the user won't have much fun showing attackers/defenders anyway. And without showing those the selected piece is the only colored (aka striped) piece making it clearly visible.
- On a color display a third color (beside attackers/defenders) works just fine.

As the Apple II doesn't have cursor-up and cursor-down keys the keys 'o' and 'l' work as alternatives to the those cursor keys.
This commit is contained in:
Oliver Schmidt 2020-01-19 14:18:58 +01:00
parent a40cccc921
commit a87f8ffca0
8 changed files with 898 additions and 1 deletions

View File

@ -9,7 +9,7 @@
# Space or comma separated list of cc65 supported target platforms to build for.
# Default: c64 (lowercase!)
TARGETS :=
TARGETS := c64 apple2
# Name of the final, single-file executable.
# Default: name of the current dir with target name appended
@ -36,6 +36,7 @@ ASFLAGS =
# Additional linker flags and options.
# Default: none
LDFLAGS =
cc65-Chess.apple2: LDFLAGS += --start-addr 0x4000 -Wl -D -Wl __HIMEM__=0xBF00
# Path to the directory containing C and ASM sources.
# Default: src

BIN
apple2/template.dsk Normal file

Binary file not shown.

3
build.cmd Normal file
View File

@ -0,0 +1,3 @@
copy apple2\template.dsk cc65-Chess.dsk
java -jar apple2\AppleCommander-win64-1.5.0.jar -p cc65-Chess.dsk chess.system sys < \cc65\target\apple2\util\loader.system
java -jar apple2\AppleCommander-win64-1.5.0.jar -as cc65-Chess.dsk chess bin < cc65-Chess.apple2

BIN
src/apple2/charset.bin Normal file

Binary file not shown.

302
src/apple2/genPieces.cpp Normal file
View File

@ -0,0 +1,302 @@
/*
* genPieces.cpp
* cc65 Chess
*
* Created by Oliver Schmidt, January 2020.
* Pieces designed by Frank Gebhart, 1980s.
*
*/
#include <fcntl.h>
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
char pieces[]=
" "
" **** ***** **** "
" * * * * * * "
" * * * * * * "
" * ***** ***** * "
" * * "
" * * "
" * * "
" * ********* * "
" ** ** "
" * * "
" * * "
" * * "
" * * "
" * * "
" * * "
" * * "
" * *********** * "
" * * "
" ******************* "
" ******************* "
" "
" "
" **** ***** **** "
" **** ***** **** "
" **** ***** **** "
" ******************* "
" ******************* "
" ***************** "
" *************** "
" ************* "
" * * "
" *********** "
" *********** "
" *********** "
" *********** "
" *********** "
" ************* "
" *************** "
" *************** "
" * * "
" ******************* "
" ******************* "
" "
" "
" * * "
" * * * * "
" * * * "
" * * "
" * * "
" * * * * "
" * * * "
" * ** * "
" * * * "
" * * "
" * **** * "
" * * * * ** "
" ** * ** * "
" * * * "
" * * * "
" * ** "
" * * "
" * ************ * "
" * * "
" **************** "
" "
" "
" * * "
" * * * * "
" ***** *** "
" ********** "
" ************ "
" **** **** ** "
" ********* ** "
" ********** ** "
" ************ ** "
" **************** "
" **************** "
" **** **** *** "
" ** ***** ** "
" ******* *** "
" ********* ** "
" ************** "
" *************** "
" * * "
" **************** "
" **************** "
" "
" "
" "
" *** "
" * * "
" * * "
" * * * "
" * * * "
" * ******* * "
" * * * "
" * * * "
" * * * "
" * * * "
" * * "
" * * "
" ******* "
" * * "
" * * "
" ***** "
" * * "
" *************** "
" ***************** "
" "
" "
" "
" *** "
" ***** "
" ******* "
" **** **** "
" ***** ***** "
" ** ** "
" ****** ****** "
" ****** ****** "
" ****** ****** "
" ***** ***** "
" *********** "
" ********* "
" * * "
" ******* "
" ******* "
" ***** "
" *********** "
" * * "
" ***************** "
" "
" "
" "
" *** "
" * * "
" *** * * *** "
" * * * * * * "
" * * * * * * "
" * * * * * * "
" * * "
" * * * * * "
" * ** *** ** * "
" * ********* * "
" * ******* * "
" * * "
" * * "
" * *********** * "
" * * "
" * * "
" * * "
" ** ** "
" *********** "
" "
" "
" "
" *** "
" ***** "
" *** ***** *** "
" ***** *** ***** "
" ****** ***** ****** "
" ***** ******* ***** "
" ******************* "
" **** **** **** **** "
" **** ** ** **** "
" **** **** "
" ***** ***** "
" *************** "
" *************** "
" ** ** "
" ************* "
" ************* "
" ************* "
" ************* "
" *********** "
" "
" "
" *** "
" * * "
" *** *** "
" ** ** "
" ** * * ** "
" * * * * * * "
" * ** ** * "
" * * * * "
" * * * * * * "
" * * * * * * "
" * * * * * "
" * * * * * "
" * * * * "
" * * * * "
" * * "
" * ********* * "
" * * "
" * * "
" ** ** "
" *********** "
" "
" "
" *** "
" * * "
" *** *** "
" ** ** "
" ** * * ** "
" **** * * **** "
" ******* ******* "
" ******* ******* "
" *** ** * * ** *** "
" *** **** **** *** "
" *** ***** ***** *** "
" **** ********* **** "
" **** ******* **** "
" ***** ***** ***** "
" *************** "
" ** ** "
" ************* "
" ************* "
" ************* "
" *********** "
" "
" "
" "
" "
" "
" "
" *** "
" * * "
" * * "
" * * "
" * * "
" * * "
" *** "
" * * "
" * * "
" ** ** "
" * * "
" * * "
" ******* "
" * * "
" *********** "
" *********** "
" "
" "
" "
" "
" "
" "
" *** "
" ***** "
" ******* "
" ******* "
" ******* "
" ***** "
" *** "
" ***** "
" ***** "
" ******* "
" ***** "
" ***** "
" ******* "
" * * "
" *********** "
" *********** "
" "
;
int main(void)
{
int i;
int f = open("pieces.bin",O_CREAT|O_TRUNC|O_WRONLY);
char c = 0;
for(i=0; i<sizeof(pieces); ++i)
{
c |= (pieces[i] == '*') << i%7;
if (i%7 == 6)
{
write(f,&c,1);
c = 0;
}
}
close(f);
return 0;
}

192
src/apple2/hires.s Normal file
View File

@ -0,0 +1,192 @@
;
; hires.s
; cc65 Chess
;
; Created by Oliver Schmidt, January 2020.
;
;
.export _hires_CharSet, _hires_Pieces
.export _hires_Init, _hires_Done, _hires_Text, _hires_Draw, _hires_Mask
.include "apple2.inc"
.include "zeropage.inc"
.import popa, popax
VERSION := $FBB3
.rodata
BASELO:
.repeat $C0, I
.byte I & $08 << 4 | I & $C0 >> 1 | I & $C0 >> 3
.endrep
BASEHI:
.repeat $C0, I
.byte >$2000 | I & $07 << 2 | I & $30 >> 4
.endrep
_hires_CharSet:
.incbin "charset.bin"
_hires_Pieces:
.incbin "pieces.bin"
.code
.proc _hires_Init
bit $C082 ; Switch in ROM
lda VERSION ; Needs ROM
cmp #$06 ; Apple //e ?
bne :+
lda #$15 ; Turn off 80-column firmware
jsr $C300 ; Needs ROM (see $CEF4)
: bit $C080 ; Back to LC bank 2 for R/O
bit TXTCLR
bit MIXCLR
bit HIRES
lda #20
sta WNDTOP ; Prepare hires_text()
rts
.endproc
.proc _hires_Done
bit TXTSET
lda #00
sta WNDTOP ; Back to full screen text
rts
.endproc
.proc _hires_Text
tax ; 'flag'
lda MIXCLR,x
rts
.endproc
.data
.proc _hires_Draw
sta src+1 ; 'src' lo
stx src+2 ; 'src' hi
jsr popax ; 'rop'
stx rop
sta rop+1
jsr popa ; 'ysize'
sta ymax+1
jsr popa ; 'xsize'
sta xmax+1
jsr popa ; 'ypos'
sta ypos+1
tax
clc
adc ymax+1
sta ymax+1
jsr popa ; 'xpos'
sta xpos+1
clc
adc xmax+1
sta xmax+1
yloop:
lda BASELO,x
sta dst+1
lda BASEHI,x
sta dst+2
xpos: ldx #$FF ; Patched
xloop:
src: lda $FFFF,y ; Patched
iny
rop: nop ; Patched
nop ; Patched
dst: sta $FFFF,x ; Patched
inx
xmax: cpx #$FF ; Patched
bne xloop
inc ypos+1
ypos: ldx #$FF ; Patched
ymax: cpx #$FF ; Patched
bne yloop
rts
.endproc
.proc _hires_Mask
stx rop ; 'rop' hi
sta rop+1 ; 'rop' lo
jsr popa ; 'ysize'
sta ymax+1
jsr popa ; 'xsize'
sta xmax+1
jsr popa ; 'ypos'
tax
clc
adc ymax+1
sta ymax+1
jsr popa ; 'xpos'
sta xpos+1
clc
adc xmax+1
sta xmax+1
yloop:
lda BASELO,x
sta src+1
sta dst+1
lda BASEHI,x
sta src+2
sta dst+2
xpos: ldy #$FF ; Patched
xloop:
src: lda $FFFF,y ; Patched
rop: nop ; Patched
nop ; Patched
dst: sta $FFFF,y ; Patched
iny
xmax: cpy #$FF ; Patched
bne xloop
inx
ymax: cpx #$FF ; Patched
bne yloop
rts
.endproc

BIN
src/apple2/pieces.bin Normal file

Binary file not shown.

399
src/apple2/platA2.c Normal file
View File

@ -0,0 +1,399 @@
/*
* platA2.c
* cc65 Chess
*
* Created by Oliver Schmidt, January 2020.
*
*/
#include <stdio.h>
#include <conio.h>
#include <string.h>
#include "../types.h"
#include "../globals.h"
#include "../undo.h"
#include "../frontend.h"
#include "../plat.h"
/*-----------------------------------------------------------------------*/
#define BOARD_PIECE_WIDTH 3
#define BOARD_PIECE_HEIGHT 22
#define CHAR_HEIGHT 8
#define LOG_WINDOW_HEIGHT 23
#define SCREEN_WIDTH 40
#define SCREEN_HEIGHT 192
#define ROP_CONST(val) 0xA980|(val)
#define ROP_BLACK 0xA980
#define ROP_WHITE 0xA9FF
#define ROP_XOR(val) 0x4900|(val)
#define ROP_CPY 0x4980
#define ROP_INV 0x49FF
#define ROP_AND(val) 0x2900|(val)
char rop_Line[2][7] = {{0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F},
{0x00, 0x40, 0x60, 0x70, 0x78, 0x7C, 0x7E}};
char rop_Color[2][2] = {{0x55, 0x2A}, {0xD5, 0xAA}};
extern char hires_CharSet[96][CHAR_HEIGHT];
extern char hires_Pieces[6][2][BOARD_PIECE_WIDTH*BOARD_PIECE_HEIGHT];
void hires_Init(void);
void hires_Done(void);
void hires_Text(char flag);
void hires_Draw(char xpos, char ypos,
char xsize, char ysize,
unsigned rop, char *src);
void hires_Mask(char xpos, char ypos,
char xsize, char ysize,
unsigned rop);
/*-----------------------------------------------------------------------*/
void plat_Drawchar(char x, char y, unsigned rop, char c)
{
hires_Draw(x,y,1,CHAR_HEIGHT,rop,hires_CharSet[c-' ']);
}
/*-----------------------------------------------------------------------*/
void plat_Drawstring(char x, char y, unsigned rop, char *s)
{
while(*s)
plat_Drawchar(x++,y,rop,*s++);
}
/*-----------------------------------------------------------------------*/
void plat_Init()
{
char i;
// Clear the hires screen
hires_Mask(0,0,SCREEN_WIDTH,SCREEN_HEIGHT,ROP_BLACK);
// Show the hires screen
hires_Init();
// Show credits and wait for key press
plat_Drawstring(2,SCREEN_HEIGHT/2-10-CHAR_HEIGHT/2,
ROP_CPY,gszAbout);
plat_Drawstring(2,SCREEN_HEIGHT/2+10-CHAR_HEIGHT/2,
ROP_CPY,"Apple][ version by O. Schmidt, 2020.");
hires_Draw(SCREEN_WIDTH/2-2,SCREEN_HEIGHT/2-50-BOARD_PIECE_HEIGHT/2,
BOARD_PIECE_WIDTH,BOARD_PIECE_HEIGHT,ROP_CPY,
hires_Pieces[KING-1][SIDE_BLACK]);
hires_Draw(SCREEN_WIDTH/2-2,SCREEN_HEIGHT/2+50-BOARD_PIECE_HEIGHT/2,
BOARD_PIECE_WIDTH,BOARD_PIECE_HEIGHT,ROP_CPY,
hires_Pieces[KING-1][SIDE_WHITE]);
plat_ReadKeys(1);
// Draw the board border
hires_Mask( 1, 12,1,8*BOARD_PIECE_HEIGHT+2*2,ROP_CONST(rop_Line[1][2]));
hires_Mask(26, 12,1,8*BOARD_PIECE_HEIGHT+2*2,ROP_CONST(rop_Line[0][2]));
hires_Mask( 2, 12,8*BOARD_PIECE_WIDTH,2, ROP_WHITE);
hires_Mask( 2,190,8*BOARD_PIECE_WIDTH,2, ROP_WHITE);
// Add the A..H and 1..8 tile-keys
for(i=0; i<8; ++i)
{
plat_Drawchar(3+i*BOARD_PIECE_WIDTH,0, ROP_CPY,i+'A');
plat_Drawchar(0,21+i*BOARD_PIECE_HEIGHT,ROP_CPY,i+'1');
}
// Setting this to 0 will not show the "Quit" option in the main menu
gReturnToOS = 1;
}
/*-----------------------------------------------------------------------*/
void plat_UpdateScreen()
{
}
/*-----------------------------------------------------------------------*/
char plat_Menu(char **menuItems, char height, char *scroller)
{
int keyMask;
char i, j;
clrscr();
hires_Text(1);
// Show the title or the scroller if that is more relevant.
cputs(scroller == gszAbout ? menuItems[0] : scroller);
// Select the first item
i = 1;
do
{
// Show all the menu items
for(j=1; j<height; ++j)
{
if(!menuItems[j])
break;
gotoxy(((j-1)%2)*SCREEN_WIDTH/2,((j-1)/2)+1);
// Highlight the selected item
if(j==i)
revers(1);
cprintf(" %s ",menuItems[j]);
if(j==i)
revers(0);
}
// Look for user input
keyMask = plat_ReadKeys(1);
if(keyMask & INPUT_MOTION)
{
switch(keyMask & INPUT_MOTION)
{
case INPUT_LEFT:
if(!--i)
i = j-1;
break;
case INPUT_RIGHT:
if(j == ++i)
i = 1;
break;
case INPUT_UP:
if(i > 2)
i -= 2;
else
i = j-1-(i-1)%2;
break;
case INPUT_DOWN:
if(i < j-2)
i += 2;
else
i = 1+(i-1)%2;
break;
}
}
keyMask &= (INPUT_SELECT | INPUT_BACKUP);
} while(keyMask != INPUT_SELECT && keyMask != INPUT_BACKUP);
hires_Text(0);
// If backing out of the menu, return 0
if(keyMask & INPUT_BACKUP)
return 0;
// Return the selection
return i;
}
/*-----------------------------------------------------------------------*/
// Draw the chess board and possibly clear the log section
void plat_DrawBoard(char clearLog)
{
char i;
if(clearLog)
hires_Mask(27,0,SCREEN_WIDTH-27,SCREEN_HEIGHT,ROP_BLACK);
// Redraw all tiles
for(i=0; i<64; ++i)
plat_DrawSquare(i);
}
/*-----------------------------------------------------------------------*/
// Draw a tile with background and piece on it for positions 0..63
void plat_DrawSquare(char position)
{
unsigned rop;
char inv;
char y = position / 8, x = position & 7;
char piece = gChessBoard[y][x];
char blackWhite = !((x & 1) ^ (y & 1));
if(piece)
{
rop = blackWhite ? ROP_INV : ROP_CPY;
inv = blackWhite ^ (piece & PIECE_WHITE) != 0;
}
else
rop = blackWhite ? ROP_WHITE : ROP_BLACK;
hires_Draw(2+x*BOARD_PIECE_WIDTH,14+y*BOARD_PIECE_HEIGHT,
BOARD_PIECE_WIDTH,BOARD_PIECE_HEIGHT,rop,
hires_Pieces[(piece&PIECE_DATA)-1][inv]);
// Show the attack numbers
if(gShowAttackBoard)
{
char val[4];
rop = blackWhite ? ROP_INV : ROP_CPY;
sprintf(val,"%02X%d",gChessBoard[y][x],(gChessBoard[y][x]&PIECE_WHITE)>>7);
plat_Drawchar(2+x*BOARD_PIECE_WIDTH,14+(y+1)*BOARD_PIECE_HEIGHT-CHAR_HEIGHT,
rop,(gpAttackBoard[giAttackBoardOffset[position][0]])+'0');
plat_Drawchar(2+(x+1)*BOARD_PIECE_WIDTH-1,14+(y+1)*BOARD_PIECE_HEIGHT-CHAR_HEIGHT,
rop,(gpAttackBoard[giAttackBoardOffset[position][1]])+'0');
plat_Drawstring(2+x*BOARD_PIECE_WIDTH,14+y*BOARD_PIECE_HEIGHT,rop,val);
}
}
/*-----------------------------------------------------------------------*/
void plat_ShowSideToGoLabel(char side)
{
plat_Drawstring(28,SCREEN_HEIGHT-CHAR_HEIGHT,
side?ROP_CPY:ROP_INV,gszSideLabel[side]);
}
/*-----------------------------------------------------------------------*/
void plat_Highlight(char position, char color, char cursor)
{
char y = position / 8, x = position & 7;
if (cursor && color != HCOLOR_SELECTED)
{
char size = color == HCOLOR_EMPTY ? 6 : color == HCOLOR_INVALID ? 4 : 2;
hires_Mask(2+x*BOARD_PIECE_WIDTH,14+y*BOARD_PIECE_HEIGHT+size,
1,BOARD_PIECE_HEIGHT-2*size,ROP_XOR(rop_Line[0][size]));
hires_Mask(2+x*BOARD_PIECE_WIDTH+BOARD_PIECE_WIDTH-1,14+y*BOARD_PIECE_HEIGHT+size,
1,BOARD_PIECE_HEIGHT-2*size,ROP_XOR(rop_Line[1][size]));
hires_Mask(2+x*BOARD_PIECE_WIDTH,14+y*BOARD_PIECE_HEIGHT,
BOARD_PIECE_WIDTH,size,ROP_XOR(0x7F));
hires_Mask(2+x*BOARD_PIECE_WIDTH,14+y*BOARD_PIECE_HEIGHT+BOARD_PIECE_HEIGHT-size,
BOARD_PIECE_WIDTH,size,ROP_XOR(0x7F));
}
else
{
char set = color == HCOLOR_ATTACK;
char val = cursor ^ x * BOARD_PIECE_WIDTH & 1;
hires_Mask(2+x*BOARD_PIECE_WIDTH,14+y*BOARD_PIECE_HEIGHT,
1,BOARD_PIECE_HEIGHT,ROP_AND(rop_Color[set][!val]));
hires_Mask(2+x*BOARD_PIECE_WIDTH+1,14+y*BOARD_PIECE_HEIGHT,
1,BOARD_PIECE_HEIGHT,ROP_AND(rop_Color[set][val]));
hires_Mask(2+x*BOARD_PIECE_WIDTH+2,14+y*BOARD_PIECE_HEIGHT,
1,BOARD_PIECE_HEIGHT,ROP_AND(rop_Color[set][!val]));
}
}
/*-----------------------------------------------------------------------*/
void plat_ShowMessage(char *str, char)
{
plat_Drawstring(26,0,ROP_CPY,str);
}
/*-----------------------------------------------------------------------*/
void plat_ClearMessage()
{
hires_Mask(26,0,7,CHAR_HEIGHT,ROP_BLACK);
}
/*-----------------------------------------------------------------------*/
void plat_AddToLogWin()
{
char y;
for(y=0; y<=LOG_WINDOW_HEIGHT; ++y)
{
hires_Mask(SCREEN_WIDTH-6,y*CHAR_HEIGHT,
6,CHAR_HEIGHT,ROP_BLACK);
if(undo_FindUndoLine(LOG_WINDOW_HEIGHT-y))
{
frontend_FormatLogString();
plat_Drawstring(SCREEN_WIDTH-6,y*CHAR_HEIGHT,
gColor[0]?ROP_CPY:ROP_INV,gLogStrBuffer);
}
}
}
/*-----------------------------------------------------------------------*/
void plat_AddToLogWinTop()
{
plat_AddToLogWin();
}
/*-----------------------------------------------------------------------*/
int plat_ReadKeys(char blocking)
{
char key = 0;
int keyMask = 0;
if(blocking || kbhit())
key = cgetc();
else
return 0;
switch(key)
{
case CH_CURS_LEFT:
keyMask |= INPUT_LEFT;
break;
case CH_CURS_RIGHT:
keyMask |= INPUT_RIGHT;
break;
case 'O':
case 'o': // Reasonably located on the ][ keyboard
case 0x0B: // No CH_CURS_UP in apple2.h, only in apple2enh.h
keyMask |= INPUT_UP;
break;
case 'L':
case 'l': // Reasonably located on the ][ keyboard
case 0x0A: // No CH_CURS_DOWN in apple2.h, only in apple2enh.h
keyMask |= INPUT_DOWN;
break;
case CH_ESC:
keyMask |= INPUT_BACKUP;
break;
case CH_ENTER:
keyMask |= INPUT_SELECT;
break;
case 'A':
case 'a': // Show Attackers
keyMask |= INPUT_TOGGLE_A;
break;
case 'B':
case 'b': // Board attacks - Show all attacks
keyMask |= INPUT_TOGGLE_B;
break;
case 'D':
case 'd': // Show Defenders
keyMask |= INPUT_TOGGLE_D;
break;
case 'M':
case 'm':
keyMask |= INPUT_MENU;
break;
case 'R':
case 'r':
keyMask |= INPUT_REDO;
break;
case 'U':
case 'u':
keyMask |= INPUT_UNDO;
break;
}
return keyMask;
}
/*-----------------------------------------------------------------------*/
// Only ever gets called if gReturnToOS is true
void plat_Shutdown()
{
hires_Done();
}