Jump to content
IGNORED

Camel99 Forth Information goes here


TheBF

Recommended Posts

After a break and some food I have to recant my evil ways. :) 

 

DO LOOP makes the whole thing so simple.

HEX
8800 CONSTANT VDPRD               \ vdp ram read data
8802 CONSTANT VDPSTS              \ vdp status
8C00 CONSTANT VDPWD               \ vdp ram write data
8C02 CONSTANT VDPWA               \ vdp ram read/write address

\ VDP set-address sub-routines
CODE 0LIMI   0 LIMI,   NEXT,  ENDCODE

: RMODE ( vdpaddr -- ) DUP 0LIMI VDPWA C! >< VDPWA C! ;
: WMODE ( vdpaddr -- ) 4000 OR RMODE ;

: VC@+  ( Vdpaddr -- c) VDPRD C@ ; \ read & inc. address
: VC!+  ( c --) VDPWD C! ; \ write & inc. address

: VC@   ( VDP-adr -- char ) RMODE VDPRD C@ ;
: VC!   ( c vaddr --) WMODE VC!+ ; \ set address and write

\ VDP integer fetch & store
: V@    ( VDPadr -- n) VC@  VC@+  FUSE  ;
: V!    ( n vaddr --) >R  SPLIT R> VC! VC!+ ;

: VWRITE ( addr Vaddr cnt -- ) SWAP WMODE 0 DO  COUNT VC!+  LOOP DROP ;
: VFILL  ( Vaddr cnt char --)  ROT WMODE  SWAP 0 DO  DUP VC!+  LOOP DROP ;
: VREAD  ( Vaddr Ram cnt --)   ROT RMODE  BOUNDS DO  VC@+ I C! LOOP DROP ;

 

  • Like 3
Link to comment
Share on other sites

Well as these things seem to go, I had trouble getting my DO LOOP code to work after it "recompiled"  as in... it didn't work. :)

That didn't seem obvious to fix so I moved ahead with getting text on the screen. 

 

This brought me back to the VDP and video screen control. I decided to simplify and roll the two together.

I changed the name VC!+  to EMIT+ because that's what it does. 

and I remembered a magic word called DUP>R 

This improves the speed of loops with a counter on the return stack. 

 

Here is the STD-OUT.FTH file.   It is pretty reasonable in terms of size for what it does. 

 

TYPE is adequately fast but VFILL would be a good candidate to recode in Forth Assembler of course.

 

Spoiler
\ Standard Forth output words
COMPILER HEX 

TARGET
8800 CONSTANT VDPRD               \ vdp ram read data
\ 8802 CONSTANT VDPSTS            \ vdp status
8C00 CONSTANT VDPWD               \ vdp ram write data
8C02 CONSTANT VDPWA               \ vdp ram read/write address

\ VDP set-address sub-routines
CODE 0LIMI   0 LIMI,   NEXT,  ENDCODE

: RMODE ( vdpaddr -- ) DUP 0LIMI VDPWA C! >< VDPWA C! ;
: WMODE ( vdpaddr -- ) 4000 OR RMODE ;
: EMIT+  ( c --) VDPWD C! ; \ write & inc. address

: VFILL   ( vaddr len c -- )
    ROT WMODE 
    SWAP >R
    BEGIN
      DUP EMIT+ 
      R> 1- DUP>R
    -UNTIL
    R> 2DROP ;

VARIABLE C/L      COMPILER 20 C/L T!

TARGET
VARIABLE COL
VARIABLE ROW
VARIABLE CURSOR
VARIABLE C/SCR     COMPILER 3C0 C/SCR T!

TARGET
20 CONSTANT BL

: CLIP   ( n lo hi -- n) ROT MIN MAX ;
: >VPOS  ( col row -- vaddr) C/L @ * + ;
: CURSOR ( -- Vaddr) COL @ ROW @ >VPOS  0 C/SCR @ CLIP ;

: COL+!  ( n -- )
  COL @ +  DUP C/SCR @ >
  IF DROP COL OFF EXIT
  THEN COL ! ;

: ROW+!  ( n -- ) ROW @ +  0 23 CLIP ROW ! ;
: EMIT   ( c --)  CURSOR WMODE EMIT+ 1 COL+! ; 
: CR     ( -- )   1 ROW+!  COL OFF ;
: SPACE  ( -- )   BL EMIT ;

: TYPE   ( addr len -- )
    CURSOR WMODE 
    >R
    BEGIN
      COUNT EMIT+  1 COL+! 
      R> 1- DUP>R
    -UNTIL
    R> 2DROP ;

: AT-XY  ( col row -- ) ROW ! COL ! CURSOR WMODE  ;
: PAGE   0 C/SCR @ 20 VFILL  0 0 AT-XY ;

 

 

So with that working I figured out how to make S"  work in a cross-compiled definition. 

That required more spells and elixirs than I bargained for but now I know how to do it. 

I tested it in this "hello world" program which is back to looking like alphabet soup. :) 

I will migrate S"  et al back into the compiler I think, but I will need a DEFER word to handle (S")

 

The program compiles to 770 bytes because of the extra features in the STD-OUT file which also required a lot of primitives to be "imported" 

But on the plus side the actual MAIN program is normal Forth code 

 

It looks like I need to make IMPORT: smarter so it only loads primitives that are not already loaded.

Then I can put import statements in the library files and forget about them.

 

Spoiler
\ TESTPROG2.FTH  Demo IMPORT: CODE  loops and AUTOSTART   Sep 2023 Fox 

HEX 2000 ORG   \ this must be set before compiling any code 

INCLUDE DSK7.ITC-FORTH \ preamble for indirect threaded Forth

\ extend the cross-compiler 1st
COMPILER ALSO META DEFINITIONS ( this holds the "immediate" words and support )
HOST: TALIGN     ( -- )   THERE ALIGNED H ! ;HOST
HOST: S,       ( c-addr u -- ) THERE OVER 1+ TALLOT PLACE  TALIGN ;HOST 

\ steal needed kernel primitives 
COMPILER WARNINGS OFF 
IMPORT: DUP 2DUP SWAP DROP 2DROP OVER ><  ROT 
IMPORT: >R R> DUP>R 
IMPORT: 1- 1+  0=  * +  > 
IMPORT: C@ C! COUNT  @  ! 
IMPORT: OR FUSE SPLIT 
IMPORT: ON OFF  MIN MAX  ALIGNED
COMPILER WARNINGS ON 

HEX 
INCLUDE DSK7.STD-OUT 

COMPILER DECIMAL 

TARGET
: (S")  ( -- c-addr u) R>  COUNT  2DUP + ALIGNED >R ; \ run-time for S" 

COMPILER ALSO META DEFINITIONS 
HOST: S"   [CHAR] " PARSE  TCOMPILE (S")  S, ;HOST IMMEDIATE 

TARGET
: MAIN  ( -- ) 
   768 C/SCR !  
   PAGE 
   S" HELLO WORLD" TYPE 
   BEGIN AGAIN 
;

COMPILER 
AUTOSTART MAIN 
SAVE DSK7.HELLOWORLD

 

 

 

 

 

 

hello_world 2023-09-15 11_37_44 PM.png

  • Like 1
Link to comment
Share on other sites

I have finally decided that the simplest way to run this "meta" compiler as they are called in Forth circles, is to IMPORT all the common Forth code primitives.

That way you don't have to do it manually and I don't have to make a program scanner that searches for them in a first pass. :) 

 

The thing is that without the dictionary headers all these words take about 1K bytes. That's a pretty small run time block. 

The current screen output file uses another ?400 bytes. 

IMPORT: DUP DROP SWAP OVER ROT -ROT NIP 
IMPORT: C!  C@ COUNT  @ !  +! C+!  2!  2@   
IMPORT: SP@  SP! RP@  RP!
IMPORT: DUP>R  >R  R>  R@   2>R 2R>  
IMPORT: ?DUP  ><  2DROP 2DUP  2SWAP PICK  
IMPORT: AND OR XOR 
IMPORT: 1+  1-  2+  2-  2*  4*  8*  2/   
IMPORT: 1+!  1-! 
IMPORT: + -  D+  
IMPORT: RSHIFT LSHIFT INVERT ABS  NEGATE  ALIGNED 
IMPORT: UM*  *  UM/MOD  M/MOD  
IMPORT: =  OVER= 0<  U<  >  < 
IMPORT: MIN  MAX SPLIT FUSE 
IMPORT: MOVE FILL SKIP  SCAN 
IMPORT: ON OFF 
IMPORT: BOUNDS /STRING 

 

So I rolled everything up into the ITC-FORTH preamble file.  Then you just need to include your I/O file. At the moment there is just STD-OUT.

And optionally you can import some of the other primitives in the system. I used ?TERMINAL in the demo below.

 

With a different file to define the dictionary headers, I should be able to rebuild the Camel99 kernel on a TI-99! 
That might be a first? A language rebuilding itself on the 99.

 

hello world looks like this.   I think it's turning into a useable thing.  

 

\ HELLO.FTH  for the recompiler.  Demo Sep 16 2023 Fox 

HEX 2000 ORG   \ this must be set before compiling any code 

INCLUDE DSK7.ITC-FORTH \ preamble for indirect threaded Forth
INCLUDE DSK7.STD-OUT

IMPORT: ?TERMINAL

COMPILER DECIMAL 
TARGET
: MAIN  ( -- ) 
   768 C/SCR !     \ init this variable 
   
   PAGE  
   S" HELLO WORLD" TYPE 

   BEGIN 
     ?TERMINAL 
   UNTIL 
   BYE 
;

\ tell the compiler what to do with this
COMPILER 
AUTOSTART MAIN 
SAVE DSK7.HELLOWORLD

HOST  \ return to HOST Forth 

\ you could automatically exit to TI-99 Main page
( BYE )  

 

  • Like 2
Link to comment
Share on other sites

After I failed to get DO/LOOP working I thought I would take a run a something simpler. 

Chuck's FOR NEXT loop running on the return stack. 

That helped me uncovered what was wrong.

(the order that you load things is critical when you have words with the same name in these @$@!# cross-compilers) :)

 

Anyway..

Here is the total code to make a FOR/NEXT loop in ANS Forth. 

HEX
CODE (NEXT)
    *RP DEC,            \ decrement loop ON RSTACK or R15
    OC IF,              \ test carry flag
        *IP IP ADD,     \ jump back: add offset value to interpreter pointer
        NEXT,
    ENDIF, 
    RP INCT,       \ remove counter from Rstack 
    IP INCT,       \ move past (LOOP)'s in-line parameter
    NEXT, 
ENDCODE

: FOR       ( n -- )  POSTPONE >R HERE ;  IMMEDIATE
: NEXT      ( -- )    POSTPONE (NEXT) HERE  -  , ; IMMEDIATE

 

And here is what it takes to implement the ANS compliant DO/LOOP 

I am now understanding why Chuck was not a big fan and why he abandoned DO/LOOP in his later years. :) 

TARGET 
CODE <?DO>  ( limit ndx -- )
        *SP TOS CMP, 
        1 $ JNE, 
        TOS POP,
        TOS POP, 
        IP RPOP,
        NEXT,

+CODE <DO>  ( limit indx -- )
1 $:    R0  8000 LI,  
        *SP+ R0  SUB,
        R0  TOS ADD, 
        R0  RPUSH,
        TOS RPUSH,
        TOS POP, 
        NEXT,
ENDCODE

CODE <+LOOP>
        TOS *RP ADD, 
        TOS POP,   
        2 $ JMP,
        
+CODE <LOOP>
        *RP INC,      
2 $:    1 $ JNO,  
        IP INCT, 
        3 $ JMP, 

1 $:    *IP IP ADD, 
        NEXT,

+CODE UNLOOP
3 $:    RP  4 AI, 
        NEXT,
ENDCODE

CODE I  ( -- n)
        TOS PUSH,        
        *RP    TOS MOV, 
        2 (RP) TOS SUB,    
        NEXT,             
ENDCODE

CODE J      ( -- n)
        TOS PUSH,
        4 (RP) TOS MOV,   \ outer loop index is on the rstack
        6 (RP) TOS SUB,   \ index = loopindex - fudge
        NEXT,
ENDCODE

VARIABLE LP 
VARIABLE L0       COMPILER  4 CELLS TALLOT 

TARGET 
: >L        ( x -- ) ( L: -- x ) 2 LP +!   LP @ ! ;     \ LP stack grows up
: L>        ( -- x ) ( L: x -- ) LP @ @  -2 LP +! ;

: RAKE  ( -- ) ( L: 0 a1 a2 .. aN -- )
        BEGIN  L> ?DUP WHILE   POSTPONE THEN  REPEAT ;


COMPILER ALSO META DEFINITIONS 
: DO        ( n n -- adr)  TCOMPILE <DO>   0 >L  POSTPONE BEGIN  ; IMMEDIATE
: ?DO       ( n n -- adr)  TCOMPILE <?DO>  0 >L  POSTPONE BEGIN  ; IMMEDIATE
: LEAVE     ( -- ) TCOMPILE UNLOOP  TCOMPILE BRANCH AHEAD  >L ; IMMEDIATE

\ complete a DO loop
: LOOP      ( -- )  TCOMPILE <LOOP>  <BACK  RAKE ; IMMEDIATE
: +LOOP     ( -- )  TCOMPILE <+LOOP> <BACK  RAKE ; IMMEDIATE

PREVIOUS DEFINITIONS 

Edit: copy paste error. I had the hi-level words twice in the file. :) 

  • Like 2
Link to comment
Share on other sites

I went down the rabbit hole of trying to refine that crude VDP screen driver for the metacompiler.

You can ( and I did) waste a lot of time playing with variations on this thing and the conclusion...

wait for it... is that CODE is much faster that Forth. :) 

 

But is was fun to see how far could push Forth. 

 

The spoiler has the "mostly Forth version. 

There are only two code words:

  1. One to disable interrupts 
  2. VC!+ which made a big difference. It writes a byte to last VDP address set & auto increments the VDP address

You can see that VFILL speed is way slower than ALC but it looks similar to GPL speed.

VFILL is only 16 bytes in Forth versus 28 bytes in ALC. The extra code is in RMODE and WMODE and they are re-useable in other words. 

 

TYPE is acceptable speed when implemented this way. 

If I put the FOR NEXT loop counter in a register it would speed up about 10..12% from my testing of FOR NEXT. 

So there it is. Stuff you already knew but now you have a video for it. 

 

Spoiler
\ STD-OUT1A.FTH output words in Forth + minimal code Sept 17 2023
 
COMPILER HEX
 
TARGET
8800 CONSTANT VDPRD               \ vdp ram read data
\ 8802 CONSTANT VDPSTS            \ vdp status
8C00 CONSTANT VDPWD               \ vdp ram write data
8C02 CONSTANT VDPWA               \ vdp ram read/write address
 
\ VDP set-address sub-routines
 CODE 0LIMI   0 LIMI,   NEXT,  ENDCODE
: RMODE ( vdpaddr -- ) DUP 0LIMI VDPWA C! >< VDPWA C! ;
: WMODE ( vdpaddr -- ) 4000 OR RMODE ;
 
 
VARIABLE C/L      COMPILER 20 C/L T!
TARGET
VARIABLE COL
VARIABLE ROW
VARIABLE CURSOR
VARIABLE C/SCR     COMPILER 3C0 C/SCR T!
 
TARGET
20 CONSTANT BL
 
\ : EMIT+  ( c --) VDPWD C! ; \ write & inc. address
 CODE VC!+ ( c --)
    TOS SWPB,
    TOS VDPWD @@ MOVB,
    TOS POP,
    NEXT,
 ENDCODE

: VFILL   ( vaddr len c -- ) 
  ROT WMODE SWAP FOR  DUP VC!+  NEXT DROP  ;
 
: >VPOS  ( col row -- vaddr) C/L @ * + ;
: CURSOR ( -- Vaddr) COL @ ROW @ >VPOS  ;
: AT-XY  ( col row -- ) ROW ! COL ! CURSOR WMODE ;
: PAGE   ( -- ) 0  C/SCR @ BL VFILL  0 0 AT-XY ;
 
: ?WRAP  ( -- ) COL @ C/SCR @ 1- > IF  0 0 AT-XY  THEN  ;
 
: ROW+!  ( n -- ) ROW @ +  23 > IF  ROW OFF EXIT  THEN ROW ! ;
: EMIT+  ( c -- ) VC!+ COL 1+! ?WRAP ;
: EMIT   ( c --)  CURSOR WMODE EMIT+ ;
: CR     ( -- )   1 ROW+!  COL OFF ;
: SPACE  ( -- )   BL EMIT ;
 
: TYPE   ( addr len -- ) 1- CURSOR WMODE FOR  COUNT EMIT+  NEXT DROP ;
 
COMPILER ALSO META DEFINITIONS 
HOST: ."   
   [CHAR] " PARSE  
   TCOMPILE (S")  TS, 
   TCOMPILE TYPE 
;HOST IMMEDIATE 


 

 

I am beginning to get this meta compiler organized and it is better than my DOS one.

I have actually learned something after all these years fighting with this stuff. 

 

Here is the test program that runs is the video. It's "normal" Forth with a few magic words to keep the compiler happy. :) 

With the CORE Forth primitives imported from Camel99 Forth, and the output library above it compiles to 1596 bytes.  

It could be smaller if I wanted to remove all the primitives that are not used. 

\ VFILLTEST.FTH   using FOR NEXT loop FOR VFILL and TYPE 

HEX 2000 ORG   \ this must be set before compiling any code 

INCLUDE DSK7.ITC-FORTH  \ preamble for indirect threaded Forth
INCLUDE DSK7.STD-OUT1A1 \ uses for/next VFILL 

IMPORT: ?TERMINAL 

COMPILER DECIMAL 

TARGET 
: VFILLTEST
   95 FOR   
      0  C/SCR @  R@ 33 + VFILL
   NEXT
;

: DELAY ( n --  ) FOR  R@ DROP NEXT ;

TARGET
: MAIN  ( -- ) 
   768 C/SCR !  
   VFILLTEST 
   5 12 AT-XY ."     VFILL in Forth    "   
   5 13 AT-XY ."   FOR  DUP VC!+  NEXT "  
   5000 DELAY 
   0 0 AT-XY 
   BEGIN 
    ." Hello metacompiling world!   "
    ?TERMINAL 
   UNTIL
   BYE 
;

COMPILER 
AUTOSTART MAIN 
SAVE DSK7.VFILLTEST3

HOST 

 

  • Like 2
Link to comment
Share on other sites

  • 3 weeks later...

And now the answer to the question I am sure you all have been asking yourselves for years. :) 

 

How does the calling overhead in Forth compare to the calling overhead in Assembly Language?

 

I had read that on some old machines Chuck's method of calling a sub-routine was faster than the native instructions for that purpose on the CPU.

(It might have been 1802 processor that exhibited this behaviour) ?

 

My work on the ASMForth system gave me a simple way to compare these things since the source for the nesting benchmark can be compiled with only minor changes. 

(ASMForth is an "Assembler" that uses Forth syntax conventions.)

Spoiler
HOST INCLUDE DSK1.ELAPSE 

ASMFORTH 
\ these are not "Forth" words. They are "ASMForth" sub-routines
: BOTTOM ;
: 1st BOTTOM BOTTOM ;  : 2nd 1st 1st ;      : 3rd 2nd 2nd ;
: 4th 3rd 3rd ;        : 5th 4th 4th ;      : 6th 5th 5th ;
: 7th 6th 6th ;        : 8th 7th 7th ;      : 9th 8th 8th ;
: 10th 9th 9th ;       : 11th 10th 10th ;   : 12th 11th 11th ;
: 13th 12th 12th ;     : 14th 13th 13th ;   : 15th 14th 14th ;
: 16th 15th 15th ;     : 17th 16th 16th ;   : 18th 17th 17th ;
: 19th 18th 18th ;     : 20th 19th 19th ;   

\ This CODE word can be called from the "HOST" Forth system 
CODE RUN    20th  ;CODE 

HOST 
:  1MILLION   CR ."  1 million nest/unnest operations" RUN ;

CR .( start demo like this: )
CR .(   ELAPSE 1MILLION  )


\ recompile with tailcall optimization operator ( -; )
ASMFORTH 
: BOTTOM  ;  \ can't optimze this one because there is no function in it. 
: 1ST BOTTOM BOTTOM -;  : 2ND 1ST 1ST -;      : 3RD 2ND 2ND -;
: 4TH 3RD 3RD -;        : 5TH 4TH 4TH -;      : 6TH 5TH 5TH -;
: 7TH 6TH 6TH -;        : 8TH 7TH 7TH -;      : 9TH 8TH 8TH -;
: 10TH 9TH 9TH -;       : 11TH 10TH 10TH -;   : 12TH 11TH 11TH -;
: 13TH 12TH 12TH -;     : 14TH 13TH 13TH -;   : 15TH 14TH 14TH -;
: 16TH 15TH 15TH -;     : 17TH 16TH 16TH -;   : 18TH 17TH 17TH -;
: 19TH 18TH 18TH -;     : 20TH 19TH 19TH -;   

CODE RUN    20TH  ;CODE 

HOST 
:  1MILLIONTC   CR ." Optimized 1M nest/unnest operations" RUN ;

 

 

Something to remember is that the normal BL instruction is fast but code called with BL by itself cannot BL to another sub-routine without manual intervention by the programmer saving the linkage register R11.

 

To build "nestable" sub-routines that can call each other many layers deep requires that we add a stack to the system and automate saving R11.

My solution was to put the saving instructions at the beginning of every sub-routine and the restore instruction at the end.

The allows the program to use BL normally but inside every sub-routine you can BL to another sub-routine with no concerns. 

 

Here is the "entry" code in front of every sub-routine

      R11 RPUSH,

 

And RPUSH, is a "pseudo-instruction" that actually does two instructions.

(RP is an alias for R7 in ASMFORTH)

        RP DECT,    \ move the return stack pointer to give us a new cell    
   R11 *RP MOV,     \ save R11 in this new cell on the return stack 

 

At the end of the every sub-routine we have these two pseudo-instructions. 

     R11 RPOP, 
         RT, 

 

These go into the code as:              

       *RP+ R11 MOV, 
           *R11 B,  

 

So here is the answer to the burning question: 

This method of Assembly Language calling is faster than Forth indirect or direct threading.

 

I put TurboForth in the list because people are more familiar with it. 

We can see that even resorting to direct threading does not match the speed of BL with a return stack. 

TI-99 Nesting Benchmark

TurboForth 1.21     Nesting 1Mil  2:29  (for reference) 
Camel99 Forth ITC   Nesting 1Mil  2:31
Camel99 Forth DTC   Nesting 1Mil  2:17

ASMForth II         Nesting 1Mil  1:29
- with tail call optimizing       0:54 

 

Now we know. 

BUT no free lunch was found! Each call uses 4 bytes in your program but a Forth call is only 2 bytes. 

 

ASMForth is here for the curious. 

ASMFORTH/demo at main · bfox9900/ASMFORTH · GitHub

 

Edited by TheBF
typo
  • Like 2
Link to comment
Share on other sites

I took a break from compilers to play with @Vorticon 's tank combat framework. 

 

I didn't write games with my TI-99 40 years ago and after I switched to Forth I just wanted to learn about how Forth did what it did. :) 

 

So 40 years later I have a few ideas that might be useful to someone who wants to write a game with a character that moves in different directions. 

I think it is worth noting that I did not have to resort to fancy data structure creating words.

Forth's simple memory control let's us put things into memory the way we need them to be very much like we do in Assembly Language.

 

Here are the relevant pieces of the tank game that move the tanks around. 

 

1. An simple way to make characters that are in different character sets. 

\ Enumerate Tank chars in different color sets 
DECIMAL
: TANKCHAR:  DUP  CONSTANT  8 + ; 

136 \ 1st tank character 
TANKCHAR: TANK0
TANKCHAR: TANK1 
TANKCHAR: TANK2
TANKCHAR: TANK3
TANKCHAR: TANK4
TANKCHAR: TANK5
TANKCHAR: TANK6
TANKCHAR: TANK7
TANKCHAR: TANK8
TANKCHAR: TANK9
TANKCHAR: TANK10
TANKCHAR: TANK11
TANKCHAR: TANK12
TANKCHAR: TANK13
TANKCHAR: TANK14
TANKCHAR: TANK15
DROP 

 

2. Put the tank characters in a counted string. Notice the 1st byte is the number of characters. 

\ put all tank chars in a counted string for searching with SCAN 
CREATE TANKCHARS ( -- Caddr)
  16 C,  
  TANK0  C, TANK1  C, TANK2  C, TANK3  C,
  TANK4  C, TANK5  C, TANK6  C, TANK7  C,
  TANK8  C, TANK9  C, TANK10 C, TANK11 C, 
  TANK12 C, TANK13 C, TANK14 C, TANK15 C,  

 

2a) Now we can identify an enemy tank like this: 

\ SCAN is a fast word to find a character in a string
: SCANFOR ( Caddr -- n)  COUNT ROT SCAN NIP ;

\ now we can test an enemy char with SCAN 
: ENEMY? ( char -- 0|n ) TANKCHARS  SCANFOR ;

 

 

3.  Make some variables that define a tank. I used "USER" variables because each tank has it's own workspace and stacks for multi-tasking.

This means inside of a task these variables only affect the little tank that you are controlling. 

\ --------------------------------------
\ user variables are local for each tank task
HEX 
30 USER KILLS     \ 
44 USER HEADING   \ compass heading is the direction control 
\ 46 USER TPAD    \ defined in Kernel
48 USER Y 
4A USER X 
4C USER DY       \ dx and dy can be accessed as a 2variable 
4E USER DX
50 USER PANZER   \ tank character 
52 USER SPEED   
54 USER MEMORY   \ pointer to circular buffer of headings for BETA chars  
56 USER MAILBOX  \ one mailbox per task
58 USER BRAIN    \ holds the address of a word that changes tank behaviour

 

 

4. make a tank char pattern for all 8 compass headings. 

\ tank patterns for 8 compass headings 

HEX
CREATE NORTH$    1010 , 547C , 7C7C , 7C44 , 
CREATE NE$       0012 , 3478 , FE3C , 1810 ,
CREATE EAST$     0000 , FC78 , 7F78 , FC00 , 
CREATE SE$       1018 , 3CFE , 7834 , 1200 ,
CREATE SOUTH$    447C , 7C7C , 7C54 , 1010 , 
CREATE SW$       0818 , 3C7F , 1E2C , 4800 ,
CREATE WEST$     0000 , 3F1E , FE1E , 3F00 ,
CREATE NW$       0090 , 583C , FE78 , 3010 ,

 

5. Put the patterns in an array that can be retrieved with one "heading" number

\ Put tank patterns in an array to access them numerically 
DECIMAL 
CREATE TANKS ( -- addr) 
   NORTH$ , NE$ , EAST$ , SE$ ,
   SOUTH$ , SW$ , WEST$ , NW$ , 

\ compass headings in clockwise order for reference 
\   0 CONSTANT NORTH
\   1 CONSTANT NE
\   2 CONSTANT EAST
\   3 CONSTANT SE
\   4 CONSTANT SOUTH`
\   5 CONSTANT SW
\   6 CONSTANT WEST
\   7 CONSTANT NW

\ select a pattern with a heading 
: ]TANK  ( heading -- Pattern-addr) CELLS  TANKS + @  ; 

 

6.  Make a way to write the correct tank pattern into the VDP pattern table for a given heading.

The user variable PANZER is the tanks's unique character.  (I was running out of ways to say the word "tank" so I used German) :) 

: TANK-SHAPE ( heading -- ) \ set pattern based on HEADING variable
  \ RAM address   VDP address   bytes 
      ]TANK      PANZER @ ]PDT   8  VWRITE ;

 

 

7.  Make an array of "vectors" for all 8 compass headings. These are added to the x,y location to move the tank in the direction it is travelling. 

Notice it uses 2@ to fetch both numbers at once.

\ double constant array of vectors, rotating clockwise like headings 
CREATE VECTORS  ( -- addr) 
 \   Y    X  
 \  ---  ---
    -1 ,  0 ,  \ north 
    -1 ,  1 ,  \ NE 
     0 ,  1 ,  \ east 
     1 ,  1 ,  \ SE 
     1 ,  0 ,  \ south 
     1 , -1 ,  \ SW 
     0 , -1 ,  \ west 
    -1 , -1 ,  \ NW

\ return the correct vectors for a given heading 
: ]VECTOR ( heading -- dx dy)  2 CELLS *  VECTORS + 2@ ;

 

8. put this all together to control the direction of a tank.  I put some protection of this word with ?HEADING but once everything is working well it could be removed.

DECIMAL 
: ?HEADING  ( n -- n ) DUP 8 0 WITHIN ABORT" Illegal heading" ;


: DIRECTION  ( heading  -- ) 
   ?HEADING 
   DUP HEADING !        \ remember the new heading       
   DUP ]VECTOR  DXDY!   \ set tank's vectors for this heading 
       TANK-SHAPE       \ set the graphic for this heading 
;     

 

9. Changing direction is just this easy because the headings are all in clockwise order. 

\ words to change the current heading 
: REVERSE ( heading -- heading') 4 -  07 AND ;
: -90DEG  ( heading -- -90 )     2-   07 AND ;
: +90DEG  ( heading -- +90 )     2+   07 AND ;
: LEFT    ( heading -- -45 )     1-   07 AND ;
: RIGHT   ( heading -- -45 )     1+   07 AND ;

: RND-HEADING    8 RND DIRECTION ;
: GO-LEFT       HEADING @  LEFT DIRECTION ;
: GO-RIGHT      HEADING @  RIGHT DIRECTION ;
: GO-BACKWARDS  HEADING @  REVERSE DIRECTION ;

 

I am pretty content with this now. 

 

 

 

  • Like 5
Link to comment
Share on other sites

Here is a binary program that runs a different version of @Vorticon 's  Combat. 

 

There are two different types of hunter tanks with 14 other "prey" vehicles.  

The prey are either GRAY "Alpha" creatures that just wander in a random direction until the hit something then pick another direction 

-or- they are the BLUE "rabbits". 

 

Rabbits are alpha creatures but they have extra "thoughts" :)  

: RABBIT-THOUGHTS 
    7  %CHANCE IF GO-LEFT      THEN 
    7  %CHANCE IF GO-RIGHT     THEN 
    1  %CHANCE IF TURN-AROUND THEN 
    2  %CHANCE IF FREEZE       THEN 
    5  %CHANCE IF 50 RND 5 + SPEED ! THEN  
; 

 

The black hunter tank can go a bit faster than the GRAY tanks but it is no smarter. 

 

The GREEN gunner picks a place and sits there waiting. Occasionally it will move to a new location.

 

Neither shooting tank has any smarts. If a target is directly in front of the gun it will shoot it. 

Sometimes it can't detect a target because of multi-tasking moving the target before the hunter gets a chance to detect it. 

I was surprised by how successful the gunner strategy is. (Except when the black tank finds him and kills him right at the beginning) :)

 

The binary files are an E/A 5 program called ALPHAHUNT

After all 16 tanks have spawned into existence, you can press FCTN 4 to break the program.

Type COLD to reboot the thing and you get a new random simulation. 

 

(There is a bug in the explosion code that can cause it to make sound after it should stop occasionally.

  Probably need to look into better Multi-tasking for shared resources OR put it on the ISR sound player) 

 

 

The source for Camel99 Forth might interest some folks. 

Spoiler
\ COMBAT.FTH 
\  Based on CCOMBAT HOST PROGRAM
\  Version 02.14.23 
\  by @VORTICON on Atariage.com 

\  Heavily modified for Camel99 Forth  2023 Brian Fox 

\ NEEDS DUMP      FROM DSK1.TOOLS 
NEEDS BYTE      FROM DSK1.DATABYTE 
NEEDS RND       FROM DSK1.RANDOM 
NEEDS COLOR     FROM DSK1.GRAFIX 
NEEDS U.R       FROM DSK1.UDOTR   \ right justified numbers 
NEEDS HZ        FROM DSK1.SOUND 
NEEDS TASK:     FROM DSK1.MTASK99
NEEDS MALLOC    FROM DSK1.MALLOC 
NEEDS MARKER    FROM DSK1.MARKER 
NEEDS VALUE     FROM DSK1.VALUES 

MARKER /ALPHA 

HEX 
: NEW-HEAP  2000 H !   H @ 2000 0 FILL ; \ reset & erase heap 

\ create a task in heap, fork it, assign Execution token
: SPAWN ( xt -- pid) USIZE MALLOC DUP>R FORK R@ ASSIGN R> ;

\ spawn n NULL tasks and compile the PIDs sequentially in memory 
: TASKS ( n -- ) 0 DO  ['] PAUSE SPAWN ,  LOOP ;

0 VALUE TASK-TBL  

\ array of process IDs 
: ]PID ( n -- PID ) CELLS TASK-TBL + @ ;

\ Enumerate Tank chars in different color sets 
DECIMAL
: TANKCHAR:  DUP  CONSTANT  8 + ; 

136 \ 1st tank character 
TANKCHAR: TANK0
TANKCHAR: TANK1 
TANKCHAR: TANK2
TANKCHAR: TANK3
TANKCHAR: TANK4
TANKCHAR: TANK5
TANKCHAR: TANK6
TANKCHAR: TANK7
TANKCHAR: TANK8
TANKCHAR: TANK9
TANKCHAR: TANK10
TANKCHAR: TANK11
TANKCHAR: TANK12
TANKCHAR: TANK13
TANKCHAR: TANK14
TANKCHAR: TANK15
DROP 

\ put all tank chars in a counted string for searching with SCAN 
CREATE TANKCHARS ( -- Caddr)
  16 C,  
  TANK0  C, TANK1  C, TANK2  C, TANK3  C,
  TANK4  C, TANK5  C, TANK6  C, TANK7  C,
  TANK8  C, TANK9  C, TANK10 C, TANK11 C, 
  TANK12 C, TANK13 C, TANK14 C, TANK15 C,  

\ compute process number (index) from a tank's ASCII character 
: 8/    3 RSHIFT ;
: >TASK# ( ascii -- n) 8/ [ TANK0 SET# ] LITERAL - ;

\ Get PID from the tank character argument 
\ This allows us to send messages to a tank when we detect it
\ in the battlefield 
: PID ( tank -- PID ) >TASK# ]PID ;

\ Named characters make it easier to remember the shapes
DECIMAL 
128 CONSTANT BKG   ( background character)
BKG CONSTANT ---   ( alias for bkg character )
130 CONSTANT SQR 
131 CONSTANT BULLET 
132 CONSTANT BOX 
001 CONSTANT DOT   ( sprite radar scanner )

\ Enumerate colors for Graphics programs
: ENUM  ( 0 <text> -- n) DUP CONSTANT  1+ ;

\ Color names from TI-Logo are more descriptive 
1  
ENUM TRANS  \ 1
ENUM BLACK
ENUM GREEN
ENUM LIME
ENUM BLUE
ENUM SKY
ENUM RED
ENUM CYAN  \ 8 
ENUM RUST
ENUM ORANGE
ENUM YELLOW
ENUM LEMON
ENUM OLIVE
ENUM PURPLE
ENUM GRAY
ENUM WHITE \ 16
DROP


\ --------------------------------------
\ user variables are local for each tank task
HEX 
30 USER KILLS     \ 
44 USER HEADING   \ compass heading is the direction control 
\ 46 USER TPAD    \ defined in Kernel
48 USER Y 
4A USER X 
4C USER DY       \ dx and dy can be accessed as a 2variable 
4E USER DX
50 USER PANZER   \ tank character 
52 USER SPEED   
54 USER MEMORY   \ pointer to circular buffer of headings for BETA chars  
56 USER MAILBOX  \ one mailbox per task
58 USER BRAIN   
60 USER SPRITE#  \ sprite # of the radar dot  

\ --------------------------------------
\ words to access the tank data 
: XY@      ( -- x y) Y 2@  ;
: XY!      ( x y --) Y 2! ;
: POSITION ( -- Vaddr) XY@ >VPOS ;

: DXDY!    ( x y --) DY 2! ;
: DXDY@    ( -- X Y) DY 2@ ;

\ random number functions 
: RNDX    ( -- x)  23 RND ;
: RNDY    ( -- y)  33 RND ;
: RANDOM  ( -- 0..7) 8 RND ; 
: RNDV    ( -- -1|0|1 ) 3 RND 1- ; \ random vector 


\ battlefield layout 
CREATE ScreenData
BYTE SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR
BYTE SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,SQR,SQR,SQR,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,SQR,SQR,SQR,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,SQR,SQR,SQR,---,---,---
BYTE ---,---,---,SQR,SQR,SQR,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,SQR,---,---,---,---,---
BYTE ---,---,---,---,---,SQR,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,SQR,SQR,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,SQR,SQR,---,---,SQR
BYTE SQR,---,---,---,SQR,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,SQR,---,---,---,SQR
BYTE SQR,---,---,---,SQR,---,---,SQR,SQR,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,SQR,SQR,---,---,SQR,---,---,---,SQR
BYTE SQR,---,---,---,SQR,---,---,SQR,SQR,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,SQR,SQR,---,---,SQR,---,---,---,SQR
BYTE SQR,---,---,---,SQR,---,---,SQR,SQR,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,SQR,SQR,---,---,SQR,---,---,---,SQR
BYTE SQR,---,---,---,SQR,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,SQR,---,---,---,SQR
BYTE SQR,---,---,SQR,SQR,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,SQR,SQR,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,SQR,---,---,---,---,---
BYTE ---,---,---,---,---,SQR,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,SQR,SQR,SQR,---,---,---
BYTE ---,---,---,SQR,SQR,SQR,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,SQR,SQR,SQR,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,SQR,SQR,SQR,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,---
BYTE ---,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,---,---,---,---,---,---,---,---,---,---,---,---,---,---,SQR
BYTE SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR
BYTE SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR,SQR
BYTE BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL
BYTE BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL ,BL

: .BATTLEFIELD ( -- ) ScreenData  VPG @  C/SCR @ VWRITE ;

\  Score display
DECIMAL 
: .KILLS ( pid -- )  KILLS LOCAL @  4 U.R ;

: .SCORE   
2 23 AT-XY ." Hunter:"  0 ]PID .KILLS 
4 SPACES   ." Gunner:"  1 ]PID .KILLS ;

\ -------------------------------------------
\ tank patterns for 8 compass headings 

HEX
CREATE NORTH$    1010 , 547C , 7C7C , 7C44 , 
CREATE NE$       0012 , 3478 , FE3C , 1810 ,
CREATE EAST$     0000 , FC78 , 7F78 , FC00 , 
CREATE SE$       1018 , 3CFE , 7834 , 1200 ,
CREATE SOUTH$    447C , 7C7C , 7C54 , 1010 , 
CREATE SW$       0818 , 3C7F , 1E2C , 4800 ,
CREATE WEST$     0000 , 3F1E , FE1E , 3F00 ,
CREATE NW$       0090 , 583C , FE78 , 3010 ,

\ Put tank patterns in an array to access them numerically 
DECIMAL 
CREATE TANKS ( -- addr) 
   NORTH$ , NE$ , EAST$ , SE$ ,
   SOUTH$ , SW$ , WEST$ , NW$ , 

\ compass headings in clockwise order for reference 
\   0 CONSTANT NORTH
\   1 CONSTANT NE
\   2 CONSTANT EAST
\   3 CONSTANT SE
\   4 CONSTANT SOUTH`
\   5 CONSTANT SW
\   6 CONSTANT WEST
\   7 CONSTANT NW

\ select a pattern with a heading 
: ]TANK  ( heading -- Pattern-addr) CELLS  TANKS + @  ; 

: TANK-SHAPE ( heading -- ) \ set pattern based on HEADING variable
  \ RAM address   VDP address   bytes 
      ]TANK      PANZER @ ]PDT   8  VWRITE ;



\ double constant array of vectors, rotating clockwise like headings 
CREATE VECTORS  ( -- addr) 
 \   Y    X  
 \  ---  ---
    -1 ,  0 ,  \ north 
    -1 ,  1 ,  \ NE 
     0 ,  1 ,  \ east 
     1 ,  1 ,  \ SE 
     1 ,  0 ,  \ south 
     1 , -1 ,  \ SW 
     0 , -1 ,  \ west 
    -1 , -1 ,  \ NW

\ return the correct vectors for a given heading 
: ]VECTOR ( heading -- dx dy)  2 CELLS *  VECTORS + 2@ ;

: ?HEADING  ( n -- n ) DUP 8 0 WITHIN ABORT" Illegal heading" ;

DECIMAL 
: DIRECTION  ( heading  -- ) 
   ?HEADING 
   DUP HEADING !        \ remember the new heading       
   DUP ]VECTOR  DXDY!   \ set tank's vectors for this heading 
       TANK-SHAPE       \ set the graphic for this heading 
;     

\ words to change the current heading 
: REVERSE ( heading -- heading') 4 -  07 AND ;
: -90DEG  ( heading -- -90 )     2-   07 AND ;
: +90DEG  ( heading -- +90 )     2+   07 AND ;
: LEFT    ( heading -- -45 )     1-   07 AND ;
: RIGHT   ( heading -- -45 )     1+   07 AND ;

\ EXPLOSION......................
HEX 
CREATE SHRAPNEL  \ :-) 
    0000 , 125C , 1E2C , 0000 , 
    0042 , 1498 , 0250 , 1400 ,
    1084 , 2200 , 1280 , 2400 ,
    2002 , 8001 , 0000 , 8104 ,
    0000 , 0000 , 0000 , 0000 ,

DECIMAL 
: EXPLODE ( char -- )
    4 NOISE GEN4 0 DB 25 MS 
    6 NOISE 10 MS 
    ]PDT 
     5 0 DO
        PAUSE 
        I 8* SHRAPNEL +  OVER 8 VWRITE 
        40 MS 
        GEN4 I 4* DB 
    LOOP 
    GEN4 -28 DB 60 MS
    GEN4 MUTE 
    DROP  
    SILENT
;

\ add coordinates to a vector 
: VECT+      ( x y dx dy -- x' y') >R ROT + SWAP R> + ;

: PUT-CHAR   ( c -- ) PAUSE POSITION VC! ;
: ERASE-TANK ( -- )   BKG PUT-CHAR ;
: .TANK      ( --)  PANZER @ PUT-CHAR ;
: XY.TANK ( x y --)  XY!  .TANK ;

\ managing tank location in x,y or VDP address form 
: NEXT-XY    ( x y -- x' y') DXDY@ VECT+ ; 
: FWD        ( -- x y) XY@ NEXT-XY ;  

: RANGE      ( x y -- x y) 0 DO NEXT-XY LOOP ;
: 3AHEAD     ( x y -- x y) NEXT-XY NEXT-XY ;

: VDP>XY     ( Vaddr -- X Y) C/L@ /MOD  ;
: NEXT-ADDR  ( Vaddr -- Vaddr') VDP>XY FWD >VPOS ;

: ADVANCE    ( -- )  
  ERASE-TANK   
  FWD XY.TANK 
  SPEED @ MS 
;

: DECAY1 ( n -- )
    -10 DB DUP MS 
    -18 DB DUP MS 
    -22 DB DUP MS 
    -26 DB     MS 
     MUTE  ;

: BOINK          440 HZ   6 DECAY1 ; 
: BONK           120 HZ   6 DECAY1 ;

\ SCAN is a fast word to find a character 
: SCANFOR ( Caddr -- n)  COUNT ROT SCAN NIP ;

\ now we can test an enemy char with SCAN 
: ENEMY? ( char -- 0|n ) TANKCHARS  SCANFOR ;

: RND-HEADING   8 RND DIRECTION ;
: GO-LEFT       HEADING @  LEFT DIRECTION ;
: GO-RIGHT      HEADING @  RIGHT DIRECTION ;
: TURN-AROUND  HEADING @  REVERSE DIRECTION ;

: THINK   BRAIN PERFORM ;

: CLEAR-AHEAD? ( -- ?)  XY@ NEXT-XY GCHAR BKG = ;

DECIMAL 
: ALPHA-MIND \ common logic for simple creatures 
    RND-HEADING     
    15 12 XY.TANK 
    BEGIN 
      BEGIN 
        CLEAR-AHEAD?
      WHILE 
        ADVANCE  
        THINK    \ this can RUN anything 
      REPEAT 
      GEN2 BONK  
      RND-HEADING 
    AGAIN 
;

: ALPHA-TANK ( --) 
    100 SPEED !  
    PANZER @ SET# GRAY RED COLOR 
    ['] PAUSE BRAIN ! 
    ALPHA-MIND  
;     

DECIMAL 
: %CHANCE ( n -- ?) 100 RND > ;  
: FREEZE  ( -- ) 2500 RND 500 + MS ;

: RABBIT-THOUGHTS 
    7  %CHANCE IF GO-LEFT      THEN 
    7  %CHANCE IF GO-RIGHT     THEN 
    1  %CHANCE IF TURN-AROUND THEN 
    2  %CHANCE IF FREEZE       THEN 
    5  %CHANCE IF 50 RND 5 + SPEED ! THEN  
; 

: RABBIT-TANK ( --) 
    100 SPEED !  
    PANZER @ SET# BLUE RED COLOR 
    15 12 XY.TANK 
    ['] RABBIT-THOUGHTS BRAIN ! 
    ALPHA-MIND 
;

: DESTROY ( char -- )
      DUP 
      PID SLEEP               \ put the task to sleep to stop it
      EXPLODE                 \ blow up the enemy char
      BKG FWD >VPOS VC!  \ erase from battlefield 
         
;

: ALPHA-HUNTER ( --)
    95 SPEED ! 
    GEN2 ( set the sound generator for this task )
    15 12 XY! 
    RND-HEADING  
    PANZER @ SET# BLACK RED COLOR 
    BEGIN 
      PAUSE 
      XY@ NEXT-XY GCHAR
      DUP ENEMY? IF DESTROY  KILLS 1+! .SCORE  ELSE 
      
      DUP BKG =  IF DROP ADVANCE               ELSE 
                 \ default actions     
                    BOINK  GEN2 MUTE 
                    RND-HEADING 
                    DROP
      THEN THEN 
      GEN2 MUTE 
    AGAIN 
;

: FIND-A-WALL ( -- )
    BEGIN 
      CLEAR-AHEAD?
    WHILE 
      ADVANCE  
    REPEAT 
;

: GO-AND-HIDE ( -- ) RND-HEADING  FIND-A-WALL  TURN-AROUND ;

: GUNNER ( -- ) \ finds a hiding place and shoots what comes past
    100 SPEED ! 
    GEN2 
    PANZER @ SET#  GREEN RED COLOR 
    15 12 XY.TANK
    GO-AND-HIDE 
    BEGIN   
      PAUSE 
      XY@ NEXT-XY GCHAR 
      DUP ENEMY? 
      IF  DESTROY  KILLS 1+! .SCORE 
      ELSE DROP  
      THEN 

      \ every so often move to a new location
      10000 RND 4 < IF GO-AND-HIDE THEN   
         
    AGAIN 
;

: TANK-BUILDER ( xt tank# --) 
    DUP DUP PID PANZER LOCAL ! 
            PID ASSIGN ;

\ assign the configured tank programs to tasks
: CREATE-TANKS 
  ['] ALPHA-HUNTER TANK0  TANK-BUILDER 

  ['] GUNNER      TANK1  TANK-BUILDER
  
  ['] ALPHA-TANK  TANK2  TANK-BUILDER
  ['] ALPHA-TANK  TANK3  TANK-BUILDER
  ['] ALPHA-TANK  TANK4  TANK-BUILDER
  ['] ALPHA-TANK  TANK5  TANK-BUILDER
  ['] ALPHA-TANK  TANK6  TANK-BUILDER
  ['] ALPHA-TANK  TANK7  TANK-BUILDER
  ['] ALPHA-TANK  TANK8  TANK-BUILDER

  ['] RABBIT-TANK TANK9  TANK-BUILDER
  ['] RABBIT-TANK TANK10 TANK-BUILDER
  ['] RABBIT-TANK TANK11 TANK-BUILDER

  ['] ALPHA-TANK  TANK12  TANK-BUILDER
  ['] ALPHA-TANK  TANK13  TANK-BUILDER
  ['] ALPHA-TANK  TANK14  TANK-BUILDER
  ['] ALPHA-TANK  TANK15  TANK-BUILDER
  
;

: DRAW-SCREEN 
    CLEAR
    RANDOMIZE
    S" 0000000000000000" BKG    CALLCHAR 
    S" FFFFFFFFFFFFFFFF" SQR    CALLCHAR 
    S" 0000001818000000" BULLET CALLCHAR 
    S" FFFFC3C3C3C3FFFF" BOX    CALLCHAR 
    S" 0000001000000000" DOT    CALLCHAR 
    BKG SET# YELLOW RED COLOR 
    PURPLE SCREEN
   .BATTLEFIELD .SCORE

    CREATE-TANKS 
;

HEX 
83D6 CONSTANT ALWAYS   \ :-) screen time-out timer

DECIMAL 
: RESTARTS ( n -- ) 
  1+ 1 DO  I ]PID RESTART  1000 MS  LOOP ;

: RUN  
  INIT-MULTI 
  HERE                 \ task table will use memory at HERE 
  NEW-HEAP 
  16 TASKS TO TASK-TBL \ Spawn 16 tasks. Assign HERE to task table
                            

  DRAW-SCREEN 
  MULTI  
    
  TANK0 PID RESTART    \ start the hunter first
  2000 MS              \ wait 2 seconds  
  14 RESTARTS          \ the gunner and the "prey" 

\ console task just updates the score and waits for break key 
  BEGIN 
    ALWAYS ON 
    .SCORE  
     PAUSE 
    ?TERMINAL 
  UNTIL
  SINGLE 
  SILENT  
  PAGE 
;

: STARTER   
  WARM  GRAPHICS  RUN   ABORT ;

  LOCK
  INCLUDE DSK1.SAVESYS 
' STARTER  SAVESYS DSK3.ALPHAHUNT

 

 

 

image.png.43155e574636b7c1b17040d0019d78b5.png

 

ALPHAHUNT ALPHAHUNU

  • Like 3
Link to comment
Share on other sites

That AI book was the basis for my hunter and prey simulation where each of the latter had a certain genetic composition which got passed on based on survival. 

I suppose you could make the same for the tanks, allowing them to evolve after each battle, with the hope that they will eventually settle on a stable genetic configuration which optimizes survival. Lots of cool options to explore here :)

image.thumb.png.921cf597fb4d9c8da488db7098fff3ff.png

  • Like 3
Link to comment
Share on other sites

I didn't know about your work on this.  Cool.

 

Yes I now have a framework and language that makes it pretty simple to create new entities.

There is a vector called the brain that can run any code needed inside the alpha loop and of course you could also just re-write the code loop for a creature.

And there is a memory pointer that can be used to remember locations and actions. 

Lots of potential. 

 

I will take a look at your program for some ideas.

 

 

  • Like 4
Link to comment
Share on other sites

Something I realized is that the world is not limited to the screen size. There is about 9K of VDP ram available for the simulation world to exist in, with my Forth environment.

The screen could be a window into that world that you could slide up and down to view what's happening. Might go there as well. 

  • Like 3
Link to comment
Share on other sites

  • 3 weeks later...

Over in the FbForth thread I put up a quick and dirty example of a queue for 16 bit data cells.

 

I dug into some old work I had done in 1990s for MaxForth where I broke Chuck's rule and tried to make a "general solution" for the byte-queue. 

I was able to make my old work better because I have a learned a few things hanging around here over the last few years. 

I am amazed though by how much more complicated it is to do it this way rather than the code over at FbForth. 

 

So I suppose if you needed 100 queues the general solution is ok. But most of the time we would never need a huge number of queues. 

 

( Circular byte queue for general purpose stuff  21MAR94 FOX )
\ originally written for MaxForth 68hc11 1993
\ re-write for Camel99 Forth 2023 

INCLUDE DSK1.TOOLS

HEX
: BYTE-Q: ( n -- <text>)
    CREATE
        DUP
        DUP 1- AND ABORT" size not power of 2"
        0 ,          ( write pointer "HEAD" )
        0 ,          ( read  pointer "TAIL" )
        0 ,          ( counter)
        DUP 1- ,     ( mask value to provide wrap around )
        ALLOT        ( data          )
        ALIGN 
;

\ fast field creator 
HEX
: FIELD: ( n - <TEXT>)  
    CREATE   ,  
    ;CODE 
       A118 , \ *W TOS ADD, 
       NEXT, 
    ENDCODE

\ field selectors for the Queue data structure 
DECIMAL
: ->HEAD ( q -- adr ) ;  IMMEDIATE   ( NOP for clarity)
2 FIELD: ->TAIL
4 FIELD: ->CNT
6 FIELD: ->MASK
8 FIELD: ->DATA 

: HEAD@+ ( q -- n)
        DUP>R ->HEAD @ DUP 
        1+ R@ ->MASK @ AND  R> ->HEAD ! ;

: TAIL@+ ( q -- n)
        DUP>R ->TAIL @ DUP 
        1+ R@ ->MASK @ AND  R> ->TAIL ! ;

: QBYTES  ( q -- n) ->CNT @ ;

: QC@    ( q -- c)
        DUP>R QBYTES 0= ABORT" Q empty"
        R@ TAIL@+ R@ ->DATA + C@ 
        R> ->CNT 1-! ;                            

: QFULL? ( q -- ?) DUP ->CNT @  SWAP ->MASK @ > ;

: QC!    ( c q -- )
       DUP>R QFULL? ABORT" Q full"
       R@ HEAD@+ R@ ->DATA +    ( calculate: [head+data]= Qaddr )
       C!                  ( store the C at adr )
       R> ->CNT 1+!        ( bump the count field)
;

: QRESET ( q -- )  DUP ->HEAD OFF  DUP ->TAIL OFF  ->CNT OFF ;

 

Here is how they are used.

( Example Queue Code: )
DECIMAL
( CREATE a 1K byte queue data structure )

256 BYTE-Q: X
128 BYTE-Q: Y

: .QSTATS  ( q -- )
        CR ." Size of Q    = "  DUP ->MASK @ 1+ . ."  bytes"
        CR ." Head pointer = "  DUP ->HEAD @ . 
        CR ." Tail pointer = "  DUP ->TAIL @ . 
        CR ." Bytes used   = "  QBYTES . 
;

( put the charset into a Queue )
: QLOAD ( queue --)
    127 BL 1+ 
    DO
      I OVER QC!
    LOOP 
    DROP ;

: QTYPE  ( queue -- )
    BEGIN
       DUP QBYTES 0> 
    WHILE
       DUP QC@ EMIT 
    REPEAT 
    DROP ;

\ usage:
X QLOAD  X QLOAD   X QTYPE 
Y QLOAD Y QTYPE 

 

Edited by TheBF
Updated code
  • Like 3
Link to comment
Share on other sites

When I added the FIELD: word to the queue today it seemed familiar.

I remembered that something similar called +FIELD it is part of the Forth 2012 proposal for data structure creation.

 

So for TI-99 rather than using DOES>  we use ;CODE and one instruction to create the offsets into data structures like this:

: +FIELD  \ n <"name"> -- ; Exec: addr -- 'addr
   CREATE OVER , +
\   DOES> @  + ;  \ Forth Version
   ;CODE      \ ~3X faster version   
HEX   A118 ,  \ *W TOS ADD, 
      NEXT,
   ENDCODE 

 

For FbForth or Turbo Forth you replace TOS with *SP  in the Assembler code. 

 

The complete file is below

Spoiler
\ forth 2012 structures
\ A.15 The optional Facility word set

DECIMAL
 : BEGIN-STRUCTURE ( -- addr 0 ) 
   CREATE
     HERE 0 0 ,        \ mark stack, lay dummy
   DOES> @             \ -- rec-len
;

: END-STRUCTURE  ( addr n --) SWAP ! ;  \ set len

: +FIELD  \ n <"name"> -- ; Exec: addr -- 'addr
   CREATE OVER , +
\   DOES> @  + ;  \ Forth Version

   ;CODE      \ ~3X faster version   
HEX   A118 ,  \ *W TOS ADD, 
      NEXT,
   ENDCODE 

DECIMAL 
\ using +field, make Forth 2012 field creators
: FIELD:    ( n1 "name" -- n2 ; addr1 -- addr2 ) ALIGNED 1 CELLS +FIELD ;
: CFIELD:   ( n1 "name" -- n2 ; addr1 -- addr2 )         1 CHARS +FIELD ;
: 2FIELD:   ( d1 "name" -- d2 ; addr1 -- addr2 ) ALIGNED 2 CELLS +FIELD ;

\ additional field types for Camel99 Forth
: CELLS:    ( n -- )  CELLS   +FIELD ;
: CHARS:    ( n -- )  ALIGNED +FIELD ;

 

 

  • Like 2
Link to comment
Share on other sites

  • 3 weeks later...

@dhe got me thinking about using SAMS memory.  I wondered how much overhead there would be to byte address every byte in a 64K segment of SAMS memory.

I added the complication of keeping two windows in RAM so that you could access and copy strings from different areas in the segment without a temp buffer. 

 

Anyway it is ton of code.  This is based on some earlier work that I did and I was able to remove a few instructions and there is also a shortcut code if the SAMS page

we need is already in RAM. 

 

It's a clever hack but I don't think I can base an editor on byte addressing SAMS memory without feeling A LOT OF pain in the speed arena.

I will have to write up a CMOVE in Forth to get a feel for it.

 

For reference looping through 64K bytes with a DO LOOP takes 29 seconds. :( 

Here is the code in case anyone can see a faster way.  I am using @Lee Stewart 's code to replace UM/MOD to compute a page# and offset. 

\ SAMS memmory access as BLOCK from Forth.  Source code   Brian Fox

NEEDS DUMP FROM DSK1.TOOLS
NEEDS MOV, FROM DSK1.ASM9900
NEEDS ELAPSE FROM DSK1.ELAPSE 

NEEDS SAMSINI  FROM DSK1.SAMSINI

HERE
\ ==========================================
\ >REAL is the SAMS manager. Converts a 64K address to a real address in RAM 
HEX
VARIABLE USE
VARIABLE SEG  1 SEG !              

CREATE WINDOWS  2000 , 3000 ,      \ windows in Low CPU RAM
CREATE PAGES       0 ,    0 ,      \ SAMS page in the buffer

4000 CONSTANT SAMS                \ base address of the SAMS card registers

CODE >REAL ( virtual -- real)
\ this code does TOS 4096 /MOD. ~25% faster than using DIV 
      TOS PUSH, 
      SEG @@ TOS MOV,    \ segment# to TOS
      TOS 4 SLA,         \ segment * 64 
      *SP R5 MOV,        \ virtual address to R5 
      R5  R0 MOV,        \ dup in R0
      R0  0C SRL,        \ r0 = address/2048
      R0 TOS ADD,        \ page# = QUOTIENT + SEGMENT
      
      R5 0FFF ANDI,      \ virtual address masked to 12 bits = remainder
      R5 *SP MOV,        \ remainder to 2nd item on stack

\ Quick search if page# is already in 1st buffer
      TOS PAGES @@ CMP,
      EQ IF,
            WINDOWS @@ TOS MOV, \ set 1st window  
            *SP+ TOS ADD,       \ add the offset
            NEXT,               \ Return to Forth
      ENDIF,

\ search if page# is in 2nd buffer
      TOS PAGES CELL+ @@ CMP, 
      EQ IF,
            WINDOWS CELL+ @@ TOS MOV, \ set 2nd window,
            *SP+ TOS ADD, 
            NEXT,    
      ENDIF,
\ *********************************************      

\ page# not in memory: Select another window 
      W    0001 LI,
      USE @@  W XOR,   \ toggle the active window 
      W  USE @@ MOV,   \ put it back in USE 

      W       1 SLA,   \ W 2* is our index into PAGES & WINDOWS 
  TOS PAGES (W) MOV,   \ store the page#
 WINDOWS (W) R1 MOV,   \ get the window to use ->R1

\ map new page into a RAM window         
      R1     0B SRL,   \ divide by 2048 = index into SAMS registers
      R12  1E00 LI,    \ cru address of SAMS
              0 SBO,   \ SAMS card on
           TOS  SWPB,  \ swap bytes on bank value
 TOS SAMS R1 () MOV,   \ load page# into SAMS card register
              0 SBZ,   \ SAMS card off

WINDOWS (W) TOS MOV,   \ return buffer on TOS
       *SP+ TOS ADD, 
                NEXT,
ENDCODE
 

: !L    ( n virtual -- ) >REAL ! ;
: C!L   ( c virtual -- ) >REAL C! ;

DECIMAL
: TEST  
   65535 0 
   DO 
     [CHAR] %  I C!L
   LOOP ;

\ Forth version using SEG @ 4096 UM/MOD  45 secs 
\ >REAL             29.4 secs 

 

  • Like 1
Link to comment
Share on other sites

While writing the previous post I realized I could remove the shortcuts and PAGES array and just read the SAMS registers to see what's already in memory.

This is smaller but it took 5 seconds longer.

Now I wonder if I can do the shortcuts by reading the SAMS register earlier 

CODE >REAL ( virtual -- real)
\ this code does TOS 4096 /MOD. ~25% faster than using DIV 
      TOS PUSH, 
      SEG @@ TOS MOV,    \ segment# to TOS
      TOS 4 SLA,         \ segment * 64 
      *SP R5 MOV,        \ virtual address to R5 
      R5  R0 MOV,        \ dup in R0
      R0  0C SRL,        \ r0 = address/2048
      R0 TOS ADD,        \ page# = QUOTIENT + SEGMENT
      
      R5 0FFF ANDI,      \ virtual address masked to 12 bits = remainder
      R5 *SP MOV,        \ remainder to 2nd item on stack

\ page# not in memory: Select another window 
      W    0001 LI,
      USE @@  W XOR,   \ toggle the active window 
      W  USE @@ MOV,   \ put it back in USE 

      W       1 SLA,   \ W 2* is our index into PAGES & WINDOWS 
 \  TOS PAGES (W) MOV,   \ store the page#
 WINDOWS (W) R1 MOV,   \ get the window to use ->R1
      R1     0B SRL,   \ divide by 2048 = index into SAMS registers

\ map new page into a RAM window         
      R12  1E00 LI,      \ cru address of SAMS
              0 SBO,     \ SAMS card on
    SAMS R1 () TOS CMP,  \ compare register value to new page# in TOS 
    NE IF,  
        TOS  SWPB,  \ swap bytes on bank value
        TOS SAMS R1 () MOV,   \ load page# into SAMS card register
    ENDIF,  
          
              0 SBZ,   \ SAMS card off
WINDOWS (W) TOS MOV,   \ return buffer on TOS
       *SP+ TOS ADD,   \ add offset from data stack  
                NEXT,
ENDCODE
 

 

  • Like 3
Link to comment
Share on other sites

OK so I have got it up to a bit more than 2X faster than my original Forth version.

I made use of BL sub-routines and re-wrote each memory operation fetch and store for SAMS as CODE words. 

 

Spoiler
\ SAMS memmory access as BLOCK from Forth.  Source code   Brian Fox

NEEDS DUMP FROM DSK1.TOOLS
NEEDS MOV, FROM DSK1.ASM9900
NEEDS ELAPSE FROM DSK1.ELAPSE 

NEEDS SAMSINI  FROM DSK1.SAMSINI

HERE
\ ==========================================
\ _real is the entire SAMS manager

HEX
VARIABLE USE
VARIABLE SEG  1 SEG ! 

CREATE WINDOWS  2000 , 3000 ,      \ windows in Low CPU RAM
CREATE PAGES       0 ,    0 ,      \ SAMS page in the each window

4000 CONSTANT SAMS                 \ base address of registers in SAMS card 

: (R1)  R1 () ; \ syntax sugar

\ 9900 sub-routine. NOT a Forth word. 
CREATE _real ( virtual -- real_addr)
\ perform TOS 4096 /MOD to compute offset,page# 
      TOS R5 MOV,        
      R5  R0 MOV,        \ dup in R0

      SEG @@ TOS MOV,    \ segment# to TOS
      TOS 4 SLA,         \ segment# * 64
      
      R0  0C SRL,        \ divide by 4096
      R0 TOS ADD,        \ page# = R0 + segment# 
      
      R5 0FFF ANDI,      \ offset= virtual masked to 12 bits 
      
\ search if page# is already in 1st buffer
    TOS PAGES @@ CMP,
    EQ IF,
        TOS WINDOWS @ LI,   \ set 1st window. ~2x FASTER using LI 
        R5 TOS ADD,         \ add the offset
               RT,          \ get out 
    ENDIF,

\ search if page# is in 2nd buffer
    TOS PAGES CELL+ @@ CMP, 
    EQ IF,
         TOS WINDOWS CELL+ @ LI, \ set 2nd window,
         R5 TOS ADD,        \ add the offset
                RT,  
    ENDIF,

\ page# not in memory: Select another window 
        W    0001 LI, 
        USE @@  W XOR,  
        W  USE @@ MOV, 
        W       1 SLA,   \ W 2* is new index into PAGES & WINDOWS 

    TOS PAGES (W) MOV,   \ remember this new page#
    WINDOWS (W) R1 MOV,  \ get the window to use into R1
            

\ map new page into a RAM window         
         R1    0B SRL,   \ divide window by 2048 = index into SAMS registers
         R12 1E00 LI,    \ cru address of SAMS
                0 SBO,   \ SAMS card on
             TOS  SWPB,  \ swap bytes on page# argument
    TOS SAMS (R1) MOV,   \ load page# into SAMS card register
                0 SBZ,   \ SAMS card off

  WINDOWS (W) TOS MOV,   \ get window into TOS
           R5 TOS ADD,        \ add the offset
                  RT,
\ ------------------------------------------------------------

CODE >REAL ( virtual -- offset page#)
      _real @@ BL, 
      NEXT, 
ENDCODE 

\ Fetch and store in virtual memory. (Long addresses)
CODE !L ( n virtual -- ) 
      _real @@ BL, 
      *SP+ *TOS MOV, 
            TOS POP, 
    NEXT, 
ENDCODE     

CODE C!L   ( c virtual -- ) 
    _real @@ BL, 
    1 (SP) *TOS MOVB,    
            SP INCT, 
            TOS POP,
    NEXT, 
ENDCODE 

CODE @L ( virtural -- n)
    _real @@ BL, 
    *TOS TOS MOV,
    NEXT,
ENDCODE     

CODE C@L ( virtual -- c)
    _real @@ BL, 
    *TOS TOS MOVB,
       TOS 8 SRL, 
    NEXT,
ENDCODE     
 
DECIMAL
\ write to low RAM (normal)
: TESTC! 
    65535 0
    DO 
      [CHAR] *  [ WINDOWS @ ] LITERAL C!
    LOOP  ; \ 13.2

: TESTC!L \ write to all 64K bytes  
   65535 0 
   DO 
     [CHAR] %  I C!L
   LOOP ; \ 21.6 secs 


\ write to a one virtual address 64K times
: TESTINPAGE 
    65535 0
    DO 
      [CHAR] & 30000 C!L
    LOOP  ; \ 21 SECs  

: TESTFETCH 
   65535 0 
   DO 
      I C@L DROP 
   LOOP ; \ 25 secs 

: MOVEL ( addr1 addr2 u --) \ 16 bit move 
       BOUNDS
       DO 
          DUP @L I !L 
          CELL+
       2 +LOOP 
       DROP 
;   

: FILLL ( addr len char -- )
       -ROT 
        BOUNDS 
        DO  
          DUP I C!L 
        LOOP 
        DROP 
;

 

 

64K byte writes to SAMS take 21.6 seconds. 

64K byte writes to regular RAM takes 13.11 seconds

 

I whipped up MOVEL and FILLL as Forth words and they are not bad.

Nowhere near as fast as the CODE equivalents. 

 

Since I have a native sub-routine I may be able to wrap _real into Assembler loops.

 

 

Edited by TheBF
fixed comment
  • Like 4
Link to comment
Share on other sites

22 hours ago, TheBF said:

While writing the previous post I realized I could remove the shortcuts and PAGES array and just read the SAMS registers to see what's already in memory.

This is smaller but it took 5 seconds longer.

Now I wonder if I can do the shortcuts by reading the SAMS register earlier.

 

Perhaps you already know this, but you must remember that the registers only contain the 4-KiB “page” numbers mapped and not the 1-MiB “bank” numbers mapped. Of course, if you are only dealing with a 1 MiB SAMS space, it doesn’t matter. However, if you test on Classic99, SAMS is 32 MiB by default. You can make it 0 by setting “sams_enabled=0” in classic99.ini, but there is currently no in-between.

 

...lee

  • Like 1
Link to comment
Share on other sites

13 minutes ago, Lee Stewart said:

 

Perhaps you already know this, but you must remember that the registers only contain the 4-KiB “page” numbers mapped and not the 1-MiB “bank” numbers mapped. Of course, if you are only dealing with a 1 MiB SAMS space, it doesn’t matter. However, if you test on Classic99, SAMS is 32 MiB by default. You can make it 0 by setting “sams_enabled=0” in classic99.ini, but there is currently no in-between.

 

...lee

Thank you. You corrected some of my code a few years back.  Since I have a 1Mb card I have not given much thought to anything bigger to honest.

But I will test this stuff on the bigger range on Classic99.

 

Something that has occurred to me is that the Forth 32 bit number format could work directly to access SAMS memory as 32bit address. 

Now that you have put the bug in my head I will report back with something that reads farther into SAMS.

:) 

 

 

  • Like 2
Link to comment
Share on other sites

Turns out the code change was almost nothing because I put the SEGment variable contents into R4, the TOS cache register. 

So I just removed that line of code and the required argument is on the top of the stack already if you put a double address on the data stack.

 

Spoiler
\ SAMS access from 32bit addresses from Forth. Brian Fox Nov 23 2023

NEEDS DUMP FROM DSK1.TOOLS
NEEDS MOV, FROM DSK1.ASM9900
NEEDS ELAPSE FROM DSK1.ELAPSE 

NEEDS SAMSINI  FROM DSK1.SAMSINI

HERE
\ ==========================================
\ BLOCK is the entire SAMS manager
HEX
VARIABLE USE
VARIABLE SEG  1 SEG ! 

CREATE WINDOWS  2000 , 3000 ,      \ windows in Low CPU RAM
CREATE PAGES       0 ,    0 ,      \ SAMS page in the each window

4000 CONSTANT SAMS                 \ base address of registers in SAMS card 

: (R1)  R1 () ; \ syntax sugar

\ 9900 sub-routine. NOT a Forth word. 
\ _real32 takes a double (32bit) address on the stack 
CREATE _real32 ( d -- real_addr)
\ perform TOS 4096 /MOD to compute offset,page# 
      R5 POP,            \ R5 holds the low bits, R4 ie TOS hold high bits 
      R5  R0 MOV,        \ dup low bits R0

      TOS 4 SLA,         \ segment# * 16
      
      R0  0C SRL,        \ divide by 4096
      R0 TOS ADD,        \ page# = R0 + segment# 
      
      R5 0FFF ANDI,      \ offset= virtual masked to 12 bits 
      
\ search if page# is already in 1st buffer
    TOS PAGES @@ CMP,
    EQ IF,
        TOS WINDOWS @ LI,   \ set 1st window. ~2x FASTER using LI 
        R5 TOS ADD,         \ add the offset
               RT,          \ get out 
    ENDIF,

\ search if page# is in 2nd buffer
    TOS PAGES CELL+ @@ CMP, 
    EQ IF,
         TOS WINDOWS CELL+ @ LI, \ set 2nd window,
         R5 TOS ADD,        \ add the offset
                RT,  
    ENDIF,

\ page# not in memory: Select another window 
        W    0001 LI, 
        USE @@  W XOR,  
        W  USE @@ MOV, 
        W       1 SLA,   \ W 2* is new index into PAGES & WINDOWS 

    TOS PAGES (W) MOV,   \ remember this new page#
    WINDOWS (W) R1 MOV,  \ get the window to use into R1
            

\ map new page into a RAM window         
         R1    0B SRL,   \ divide window by 2048 = index into SAMS registers
         R12 1E00 LI,    \ cru address of SAMS
                0 SBO,   \ SAMS card on
             TOS  SWPB,  \ swap bytes on page# argument
    TOS SAMS (R1) MOV,   \ load page# into SAMS card register
                0 SBZ,   \ SAMS card off

  WINDOWS (W) TOS MOV,   \ get window into TOS
           R5 TOS ADD,        \ add the offset
                  RT,
\ ------------------------------------------------------------

CODE >REAL ( virtual -- real)
    _real32 @@ BL, 
    NEXT, 
ENDCODE 

 

 

So I added 32bit fetch and store to complete the word set. Now this works like my old HsForth for MsDOS, by the late James Kalihan of Ohio.

I can reach out into the 32M range on Classic99 and fetch and store 32bit integers. 

 

Bytes work as expected but for ints and doubles you must align the address to the data size.

If not you run the risk of writing across a page boundary and parts you your number will not store correctly. 

 

\ Fetch and store "LONG" in virtual memory require a double integer address 
CODE !L ( n d -- ) 
    _real32 @@ BL, 
    *SP+ *TOS MOV, 
          TOS POP, 
    NEXT, 
ENDCODE     

CODE 2!L ( d d -- )
    _real32 @@ BL, 
    *SP+ *TOS  MOV,   
    *SP+ 2 (TOS) MOV,  
    TOS POP,
    NEXT,
ENDCODE

CODE C!L   ( c d -- ) 
    _real32 @@ BL, 
    1 (SP) *TOS MOVB,    
            SP INCT, 
            TOS POP,
    NEXT, 
ENDCODE 

CODE @L ( d -- n)
    _real32 @@ BL, 
    *TOS TOS MOV,
    NEXT,
ENDCODE     

CODE 2@L ( d -- d)
    _real32 @@ BL, 
    2 (TOS) PUSH, 
    *TOS TOS MOV, 
    NEXT,        
ENDCODE

CODE C@L ( d -- c)
    _real32 @@ BL, 
    *TOS TOS MOVB,
       TOS 8 SRL, 
    NEXT,
ENDCODE     

 

I have to use a prefix that I wrote (D#) for doubles because Camel Forth doesn't handle doubles in the kernel. 

But the screen image shows the system storing and fetching a 32bit integer, at address >1FF0000  (33,388,896) 

:)

 

 

SAMS-32bit-addressing.png

Edited by TheBF
fixed comment
  • Like 2
Link to comment
Share on other sites

  • 1 month later...

I found another place where Forth is discussed on Discord. 

Someone mentioned tail call optimization so I showed how I did it with a word I called GOTO ( *IP IP MOV,)

Another poster asked "Couldn't you use BRANCH?'"

 

After a second I realized I just had to convert the absolute address that GOTO uses, into an offset that Camel Forth BRANCH uses.

And when I tried it it made the optimization ~5% faster because BRANCH in Camel99 lives in scratch-pad RAM. :)

 

\ tail call optimizing semicolon for Camel99 Forth  Nov 27 2022 Brian Fox

DECIMAL 
: CELL-   2- ;
: PREVXT ( -- XT)  HERE CELL- @ ; \ fetch the XT of previous compiled word

\ -; does not end with EXIT because it is branching directly to another
\ list of tokens. That other list will end in EXIT or NEXT.
: -;  ( -- ) \ programmer controlled
      PREVXT >BODY              \ get previous XT, compute PFA
     -2 ALLOT                   \ erase the previous XT
      POSTPONE BRANCH HERE - ,  \ compile BRANCH to the PFA
      POSTPONE [                \ turn off compiler
      REVEAL
      ?CSP
; IMMEDIATE

: COLON?  ( xt -- ?) @  [ ' DOCOL @ ] LITERAL = ;

VARIABLE TAILCALL  \ control tail call optimizizing with this variable
                   \ TAILCALL ON  turns optimizer on

: ;   ( -- )
     TAILCALL @ 
     IF 
         PREVXT COLON?
         IF   POSTPONE -;
         ELSE POSTPONE ;
         THEN 
      ELSE 
         POSTPONE ; 
      THEN 
; IMMEDIATE

 

  • Like 2
Link to comment
Share on other sites

Happy New Year Everybody :party:

 

I was looking at some old code I built for HsForth based on the "Let's Build a Compiler" by Jack Crenshaw

Let's Build a Compiler! (penguin.cz)

It builds a tiny Pascal compiler and one day I may get enough energy to port it to TI-99.

 

I wondered about putting the keyword table and the symbol table in VDP RAM.  You can cram a lot of text into memory

using counted strings as a poor-mans linked list.  

 

Somebody might want something like this for another purpose and it should port over to the other Forth systems with a few word replacements.

If you hit a wall just ask. 

VC!    -> VSBW
VC@    -> VSBR
VWRITE -> VMBW
POSTPONE -> COMPILE

 

I have a little VDP manager library that I include to begin

\ VARIABLE VP    ( moved to kernel in V2.55 )

HEX 1000 VP !   \ "VDP pointer" start of free VDP RAM
: VHERE   ( -- addr) VP @ ;   \ FETCH the value in VDP pointer
: VALLOT  ( n -- )   VP +! ;  \ add n to the value in VDP pointer
: VC,     ( n -- )   VHERE VC!  1 VALLOT ;
: V,      ( n -- )   VHERE V!   2 VALLOT ;
: VCOUNT  ( vdp$adr -- vdpadr len ) DUP 1+ SWAP VC@ ;
: VCREATE ( <text> -- ) VHERE CONSTANT  ; \ address when <text> invoked
\ : VPLACE   ( $addr len Vaddr -- )  \ like PLACE for VDP RAM. In KERNEL 2.6
\           2DUP VC! 1+ SWAP VWRITE ;

 

Spoiler has the implementation:

Edit: I added protection to ADD$ so it doesn't go past the allocated size of the array.

Spoiler
\ compact string tables in VDP RAM          Jan 2024 Brian Fox

\ NEEDS DUMP   FROM DSK1.LOWTOOLS
NEEDS VCOUNT FROM DSK1.VDPMEM

\ "place" string caddr/u in VDP memory as byte-counted string
: VS,  ( caddr u -- ) VHERE OVER 1+ VALLOT VPLACE ;

\ compile a string into VDP memory
: ,"       [CHAR] " PARSE  VS, ;

: NEXT$   ( $addr -- $addr')  VCOUNT + ;
: NTH$    ( $list n -- $addr)  0 ?DO  NEXT$  LOOP ; \ GOTO the nth string

\ syntactic sugar. Get length of a string
: VLEN  ( $addr -- ) POSTPONE VC@ ; IMMEDIATE 

\ compile null string (0) to start list 
: VDP{  ( -- VDPaddr) VHERE   0 VC, ; 

\ compile 0 to end array
: }VDP  ( Vaddr -- Vaddr size )
    0 VC,            \ end with a null string   
    VHERE OVER -  ;  \ compute the size in bytes 

\ tables are recorded as a 2CONSTANT 
: 2CONSTANT CREATE , ,  DOES> 2@ ;

DECIMAL
\ Neil Baud's COMPARE modified to compare RAM string to VDP string
\  0 means adr1 = adr2
\ -1 means adr1 < adr2
\  1 means adr1 > adr2
: VCOMPARE  ( addr u1 Vaddr u2 -- -1|0|1 )
    ROT  2DUP - >R            ( a1 a2 n2 n1) ( R: n2-n1)
    MIN                       ( a1 a2 n3)
    BOUNDS  ( loop index I becomes the VDP address)
    DO                        ( a1)
        COUNT  I VC@ -        ( a1 diff)
        DUP IF
            NIP 0< 1 OR       ( -1|1)
            UNLOOP
            R> DROP 
            EXIT
        THEN                  ( a1 diff)
        DROP                  ( a1)
    LOOP
    DROP                      ( )
    R> DUP IF  0> 1 OR  THEN  \ 2's complement arithmetic
;

\ LOOKUP Returns index into the table or zero 
: LOOKUP ( addr len table size -- ndx )
      DROP 
      NEXT$ -ROT   
      PAD PLACE 
      1 SWAP 
      BEGIN
        DUP VLEN
      WHILE ( string<>0)
        DUP VCOUNT PAD COUNT 2SWAP VCOMPARE 
      WHILE ( Vcompare<>0)
        NEXT$
        SWAP 1+ SWAP 
      REPEAT
      THEN
      ( -- ndx Vaddr )
      VLEN 0>      \ if string length=0 we hit the end 
      ( ndx ?) AND  \ and ndx with flag  
;

\ create a table of strings that you can add to easily
: TABLE: ( n -- )
  CREATE
    VHERE               \ VDP address of the table 
    SWAP DUP ,  VALLOT  \ record size, allocate VDP space
    ,
    0 VC,               \ compile null string in the table
  DOES> 2@   ( -- Vaddr size)
;

: SEEKFREE ( vdp[] size-- Vaddr)      
  BOUNDS   ( -- last 1st )
  NEXT$    \ skip the first null 
  BEGIN 
    2DUP > 
  WHILE ( last > 1st)
    DUP VLEN 
  WHILE 
    NEXT$
  REPEAT 
  THEN 
  NIP 
;  

: ADD$  ( addr len vdp[] size -- ) 
  2DUP + >R   \ Rpush end of array 
  SEEKFREE ( -- addr len Vaddr) 
  2DUP + R> > ABORT" Can't ADD$" 
  DUP>R VPLACE R> VALLOT ;   

: NEW  ( Vaddr size -- ) 0 VFILL ; 
\ \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

 

 

And here is what it looks like when you use it.

\                   >> TEST CODE <<
\ reset the VDP memory pointer to your chosen first address
HEX 1000 VP !

\ create a finite size list of string constants
VDP{
  ," IF"  ," ELSE"  ," ENDIF"
  ," WHILE"  ," ENDWHILE"
  ," DO"   ," ENDDO"
  ," LOOP" ," ENDLOOP"
  ," REPEAT" ," UNTIL"
  ," FOR" ," TO" ," ENDFOR"
  ," BREAK"
  ," READ" ," WRITE"
  ," VAR" ," END"
  ," PROCEDURE"
  ," PROGRAM"
}VDP 2CONSTANT KEYWORDS

S" REPEAT"  KEYWORDS LOOKUP .
S" absent"  KEYWORDS LOOKUP .


1024 TABLE: SYMBOLS   
SYMBOLS NEW

S" symbol1"  SYMBOLS ADD$
S" symbol2"  SYMBOLS ADD$
S" symbol3"  SYMBOLS ADD$
S" symbol4"  SYMBOLS ADD$
S" symbol5"  SYMBOLS ADD$
S" symbol6"  SYMBOLS ADD$
S" symbol7"  SYMBOLS ADD$
S" symbol8"  SYMBOLS ADD$
S" symbol9"  SYMBOLS ADD$
S" symbol10"  SYMBOLS ADD$
S" symbol11"  SYMBOLS ADD$
S" symbol12"  SYMBOLS ADD$
S" symbol13"  SYMBOLS ADD$
S" symbol14"  SYMBOLS ADD$


\ test code to view the tables 
: VTYPE  ( Vaddr len --) BOUNDS ?DO I VC@ EMIT LOOP ;
: VPRINT ( V$ -- ) VCOUNT CR VTYPE ; 

: .TABLE ( table size -- )
  BOUNDS 
  NEXT$ 
  BEGIN 
    2DUP >
  WHILE 
    DUP VLEN
  WHILE 
    DUP VPRINT
    NEXT$ 
  REPEAT
  THEN 
  2DROP 
;

 

  • Like 2
Link to comment
Share on other sites

In the VDP packed string arrays in the previous post, I changed ADD$ to give it protection from writing past your allocated TABLE size. 

Much better. :) 

 

: ADD$  ( addr len vdp[] size -- ) 
  2DUP + >R   \ Rpush end of array 
  SEEKFREE ( -- addr len Vaddr) 
  2DUP + R> > ABORT" Can't ADD$" 
  DUP>R VPLACE R> VALLOT ;   

 

  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...