Jump to content
IGNORED

Chess


Andrew Davie

Recommended Posts

Prepping for switch over to the 3E+ version; I think I'm about 2-3 days away from having it working properly.

Then it will be interesting to see how much performance increases. I think I predicted 10% faster. As I've been modifying some of the relevant routines, and noting the changes, perhaps it won't be quite that much different; we shall see. As a prelude to the switchover, I did a simple metric on the 3E version, and that was to set it to 6PQ8 and do P-K4 and see how many nodes it had evaluated after 100 seconds. And that was 97,417.  I then did the same with P-Q4, and got 56,475. And again with P-KR4 to get 107,457 nodes. Clearly it varies based on position complexity; in other words, the number of moves. So, averaging the three I get 871 nodes per second.  That's not bad for a little 6507.  So, what's a "node"?  It's a position in a game. To get to a position you need to list all the moves from the current position, and then make one of the moves. Also included is the time to "unmake" the move to get back to the previous node. So, 871 nodes per second.  I'd be very stoked to get that up to 1000 in 3E+.

Apparently this is a standard measure (nodes/second) in chess engines. I quote.. "Modern engines give around 10-100 million nodes per second using one core of a modern processor. This program apparently searches 20 billion nodes per second using a modern GPU instead of a CPU."  So, that's a good look/feel for how this program fares compared to the modern beasts. About, let's say, one hundred thousand times slower :)

 

 

Edited by Andrew Davie
  • Like 2
Link to comment
Share on other sites

 

5 hours ago, Andrew Davie said:

one hundred thousand times slower

That suits me just fine.

 

Modern engines on current processors are something way beyond my level, so I've lost interest.  The 6507 may be slower but it's closer to my ability running your engine and it more closely resembles a human opponent .  It's also more interesting seeing it play non contemporary moves.

Link to comment
Share on other sites

Here's an interesting thing about the search process...

 

alpha-beta pruning benefits from having the best move first, when searching. Good moves early on lead to 'pruning' of the search tree. In particular, if you already have a better move, then why bother searching any further if the opponent can make things worse for you. That's the basic principle.

 

So, I have alpha-beta implemented (in the form of a negamax search). It's reduced the branching factor from something like 30 down to about 15.  That is, average moves in any position are about 30-35 for chess. But instead of searching all of these, if you implement alpha-beta then in fact many of them can be ignored. Most of them, actually. An average of 30 per position down to maybe 15 per position (as my engine currently stands). My earlier estimates seem to be off. 15 is closer than 8 :). All I do to put "good" moves first in the search is simply examine captures first. That's it.

 

All well and good.  The crux is having good moves early in the search. But how do you know a move is good, because you need a search to do that?

 

Consider a branching factor of 15. The number of positions at any depth is 15^depth.  So, on the first 'ply' that's just 15 positions. But on the second ply, it's 225. And on the third, 3375... then 50625. It gets big quickly. Imagine 30 though!  900 positions on the 2nd ply.  Clearly, reducing the average moves/ply is going to make huge differences to the search.


Earlier I did some measurements which showed the 3E engine as it currently stands is doing something like 850 nodes/second.  So, a 4-ply search would take 15^4/850 = 60 seconds. This is close to what the engine actually takes, when the alpha-beta is working well.

 

A 5-ply... (15^5)/850 = 900 seconds, give or take.  15 minutes.

If the branching factor was reduced by 1 then 5-ply would be 632 seconds.

If the branching factor was halved to about 7.5, then 5-ply would be 27 seconds.

That's a huge difference.

 

So, how do we get the average branching factor down?  Clearly, reducing the branching factor makes a major difference.

 

The trick is, as noted, put the good moves first in the search.

 

Now consider how many nodes the current engine will do in a 5-ply search. That's 15^5 = 759375 nodes.

Consider just a 2-ply search. That's 15^2 = 225 nodes.

 

Let's try doing a 2-ply search first. At that point we have a depth-2 analysis of the "value" of each of the moves. It's not going to be perfect, but it's going to be a hell of a lot better than just putting the "capture" moves first and leaving the rest unsorted.  A 2-ply search would take 225/850 = 0.25 seconds. Barely noticeable.  So, now the process is (a) do a 2-ply search, (b) sort the moves based on the score of each move, (c) do a 5-ply search.

 

The total nodes examined is (theoretically) now 759375+225.  A bit more.  But wait. Now we have the moves sorted we're going to get many many more alpha-beta cutoffs. Gobs more is the technical term. The branching factor is going to reduce from 15 to (and here, a wild guess...) 6.  It's hard to say, but it's going to be a LOT.  Instead of 15^5 nodes in the "main" search, we're now going to be doing 6^5 nodes. That's 7776 nodes.  We can do that in 7776/850 = 9 seconds.  Down from 15 minutes.  Now, 6 is optimistic. But you get the idea.  We spent extra time doing a 2-ply search first, and then the sort, then the main search.

 

But wait, why 2 ply?  What if we did a 3-ply search first. If we had zero benefit, we'd be doing 15^5 + 15^3 nodes, Which would be still.. about 900 seconds.  In other words, doing a 2-ply or 3-ply search before doing your main search is going to cost nothing, in the long run, and in fact actually significantly reduce the search time.  


So, that's what will happen when you do a search/sort before doing the main search - at the top level.  But why not extend this, and on ply2 do the same thing?  Definitely that search on ply 2 will improve the speed for each of the moves at that level. So, now we have (a) 2 ply search, (b) sort, (c) 5 ply search.  And the 5-ply search which is now starting with a sorted movelist will be (a) make a move, (b) 2-ply search, (c) sort, (d) 4-ply search.... etc.

 

 

So, that's all very interesting. Do more searching to make the searching go quicker.

 

I'll be implementing something along these lines (starting with just the 2-ply search on the top level) and do some measurements to see how the predictions/estimates stack up. In the new 3E+ implementation, which is coming along nicely. It's not there yet, but likely just a day or two away. I have the UI done - you can select pieces, from/to etc. And it's generating the moves for the player correctly so you can only select valid moves.  That's most of the hard work.  The search for computer moves is next.  The conversion process has been mostly smooth, though a bit more complex than I expected. Basically the "chunks" of code all work fine, but I have to make sure the data and subroutines they are accessing are in the correct banks. Just bringing everything back up is a stepwise process, and it's taking some time and debugging. Going well, though, no roadblocks.

First, as noted earlier, some tests on the speed of the new engine which I had previously predicted as being a 10% speedup. Maybe optimistic. And after that, then I'll get into fixing up the missing stuff (en passant,castling across check) and then finally I'll start work on the pre-search move sorting as described above.

 

 

 

 

 

 

 

 

  • Like 7
Link to comment
Share on other sites

Really great read thanks Andrew! 

 

The great coding massage where you push and prod things then branch off and try something different then mix and match. So interesting to know your thoughts about how you are progressing ?

Link to comment
Share on other sites

The listing of Chess3E+, as generated by dasm, is currently 16666 lines. Of course I didn't type all those - many are generated because of macro expansion, etc.  But it does give some idea, at least of the complexity.  By comparison, Boulder Dash was 23288 lines and Sokoboo is 28793 lines.

 

Edited by Andrew Davie
  • Like 2
Link to comment
Share on other sites

Extremely extremely early days, and there is an issue with quiescence.

BUT, just as a teaser... original 3E bankswitching 6PQ8 with P-K4 was 97,417 nodes in 100 seconds.

This 3E+ version (again, with a dodgy quiescence) first measurement shows 113,348 nodes in 100 seconds.

That's (again, just preliminary - don't quote me on this) that would be roughly 15% faster.

Well!  That's hopeful :).

Here's a 3-ply version - 3E+ bankswitching - with NO quiescence.

It will play terribly, but just a demo that the basic systems appear to be (mostly) functional.

Castling is disabled, I haven't gotten around to getting that up and running yet.  Basically it's just been 5 days or so of seemingly endless debugging. Been a hard slog, actually. The conversion wasn't nearly as simple as I expected it to be. But, as noted, seems to be going OK now.

 

Now I can start to bring things back online, and finally start moving forward again.

 

 

 

 

 

chess3E+20200512.3E+

  • Like 3
Link to comment
Share on other sites

1 hour ago, Andrew Davie said:

Extremely extremely early days, and there is an issue with quiescence.

BUT, just as a teaser... original 3E bankswitching 6PQ8 with P-K4 was 97,417 nodes in 100 seconds.

This 3E+ version (again, with a dodgy quiescence) first measurement shows 113,348 nodes in 100 seconds.

That's (again, just preliminary - don't quote me on this) that would be roughly 15% faster.

Well!  That's hopeful :).

Here's a 3-ply version - 3E+ bankswitching - with NO quiescence.

It will play terribly, but just a demo that the basic systems appear to be (mostly) functional.

Castling is disabled, I haven't gotten around to getting that up and running yet.  Basically it's just been 5 days or so of seemingly endless debugging. Been a hard slog, actually. The conversion wasn't nearly as simple as I expected it to be. But, as noted, seems to be going OK now.

 

Now I can start to bring things back online, and finally start moving forward again.

 

 

 

 

 

chess3E+20200512.3E+ 31 kB · 4 downloads

 

Just tested your ROM on the PlusCart, it works fine apart from:
- auto detection does not work, it seems to miss the "TJ3E" signature inside the ROM
- the correct 3E+ file extension for Stella and PlusCart is "3EP"

 

and it really plays terribly

 

 

  • Like 1
Link to comment
Share on other sites

2 minutes ago, Al_Nafuur said:

 

Just tested your ROM on the PlusCart, it works fine apart from:
- auto detection does not work, it seems to miss the "TJ3E" signature inside the ROM
- the correct 3E+ file extension for Stella and PlusCart is "3EP"

 

and it really plays terribly

 

 

 

Great, thanks for the feedback. I couldn't find the autodetect signature documented, so I'll put that in now.

I was certain I read that the 3E+ extension for Stella is 3E+

 

Link to comment
Share on other sites

1 minute ago, Andrew Davie said:

I couldn't find the autodetect signature documented, so I'll put that in now.

A coworker of me once said:

Don't read the documentation, read the code. The code don't lie.

  • Like 1
  • Haha 1
Link to comment
Share on other sites

I tried around today with version "chess20200505_3pq8b.bin". Really great so far. Somehow amazing, how it plays on a hardware released in 1977.  :thumbsup: The only thing, which confuses me a little bit while playing against the program, is that honeycomb-look of the blue fields. Having normal squares in two colors, separated by another color, i would prefer, to be true. It would give a better overview, at least for me.

 

At the moment the blue fields looking smaller than the black ones, cause the field-dividing-lines are also black and this disturbs me while playing. In chess, i am not used to, that one color (black) dominates the overall-look of the playing-field so clearly.

 

Can there be done someting, or has it a special technical reason, why the playing-field looks like it is at the moment? Like for example, a lack of colors which can be used permanently on the screen and therefore maybe no color left, for dividing lines in different color?

Edited by AW127
Link to comment
Share on other sites

1 minute ago, AW127 said:

I tried around today with version "chess20200505_3pq8b.bin". Really great so far. Somehow amazing, how it plays on a hardware released in 1977. The only thing which confuses me a little bit while playing against the program, is that honeycomb-look of the blue fields. Having normal squares in two colors, separated by another color, i would prefer, to be true. It would give a better overview, at least for me.  :)  At the moment the blue fields looking smaller than the black ones, cause dividing-lines are also black and this disturbs me while playing. I am not used to the fact on a chessboard, that one color (black) dominates so clearly on the playing-field.

 

Can there be done someting, or has it a special technical reason, why the playing-field looks like it is at the moment? Like for example, a lack of colors which can be used permanently on the screen and therefore maybe no color left, for dividing lines in different color?

 

Thank you for the feedback. I can make the "blue fields" square instead of rounded. However, I cannot add another colour. The blue is actually used to make the white pieces (which are a combination of 3 colours). The other two colours of the white go to make the "black" pieces.  They are inextricably entwined.  If I change the blue (which I can do), then the white changes to some other colour.  The current colours I am using offer the highest contrast I have found so far.  Remember, there is only ONE colour on any single scanline!

 

 

Link to comment
Share on other sites

Wow, that was a fast answer.  :) I was just about to rewrite some sentences a little bit in last entry (cause i don't liked my first translation), then the answer was already there.

 

11 minutes ago, Andrew Davie said:

 

I can make the "blue fields" square instead of rounded.

 

 

This would be great. I guess, that i am not the only one, which prefers square fields here in this case, cause it's the normal look for chessboards and people are used to it.

 

11 minutes ago, Andrew Davie said:

 

However, I cannot add another colour. The blue is actually used to make the white pieces (which are a combination of 3 colours). The other two colours of the white go to make the "black" pieces.  They are inextricably entwined.  If I change the blue (which I can do), then the white changes to some other colour.  The current colours I am using offer the highest contrast I have found so far.  Remember, there is only ONE colour on any single scanline!

 

 

I understand. That's what i had in mind and feared, that it could be a technical limit of permanent usable colors at the same time. But i think, making the fields in a square-look again, will help for better overview anyway, even when black is used for both, fields and the dividing-lines of the fields. At the moment, with the honeycomb-look, the blue fields, which are a little bit smaller than the black fields anyway (cause black is also the color for the dividing-lines) looks additionally downsized because of that honey-comb shape.  :)

Edited by AW127
Link to comment
Share on other sites

1 hour ago, AW127 said:

The only thing, which confuses me a little bit while playing against the program, is that honeycomb-look of the blue fields. Having normal squares in two colors, separated by another color, i would prefer, to be true. It would give a better overview, at least for me.

 

At the moment the blue fields looking smaller than the black ones, cause the field-dividing-lines are also black and this disturbs me while playing. In chess, i am not used to, that one color (black) dominates the overall-look of the playing-field so clearly.

Yes this.

 

1 hour ago, AW127 said:

Can there be done someting, or has it a special technical reason, why the playing-field looks like it is at the moment? Like for example, a lack of colors which can be used permanently on the screen and therefore maybe no color left, for dividing lines in different color?

This non-standard color scheme was done simply to differentiate between versions. IMHO it's not good for playing either. It subtly affects my playing and I'm not likely to get interested in the game.

 

Link to comment
Share on other sites

On 5/8/2020 at 12:10 AM, Andrew Davie said:

Then it will be interesting to see how much performance increases. I think I predicted 10% faster. As I've been modifying some of the relevant routines, and noting the changes, perhaps it won't be quite that much different; we shall see. As a prelude to the switchover, I did a simple metric on the 3E version, and that was to set it to 6PQ8 and do P-K4 and see how many nodes it had evaluated after 100 seconds. And that was 97,417.  I then did the same with P-Q4, and got 56,475. And again with P-KR4 to get 107,457 nodes. Clearly it varies based on position complexity; in other words, the number of moves. So, averaging the three I get 871 nodes per second.  That's not bad for a little 6507.


Well, some good news and some bad news. The bad news is that, dumb me, did those metrics with stella set to run at 300% speed. So, there's that. Basically divide by 3 to get actual nodes per second. But, in the long run it's just a comparative measurement I'm after, so here are some measurements on the new engine after I've had a chance to optimise things...

 

P-K4 is now 164,289 nodes (vs. 97,417)

P-Q4 is now 124,504 nodes (vs 56,475)

P-KR4 is now 153,082 nodes (vs 107,457)

 

So, that comes to an average of  1473 nodes/second (at 300% stella) for 3E+ implementation, vs. 871 nodes/second for the 3E version.

That is a 70% speedup.  Of course this is a bit rough, but who would have thunk? I'll subject this to the proviso that I'm always getting these measurements wrong!

 

The 3E+ scheme has allowed me to organise things in a much more efficient manner, and I've done a LOT of optimising, particularly in the "makeMove" and "unmakeMove" subroutines - which are both called once for each node, of course - and that is showing in these results. Just to show coding, here's the "unmakeMove" which is looking pretty slick these days...

 

    DEF unmakeMove
    SUBROUTINE

        REFER selectmove ;
        REFER ListPlayerMoves ;
        REFER quiesce ;
        REFER negaMax ;
        VEND unmakeMove

    ; restore the board evaluation to what it was at the start of this ply
    ; TODO: note: moved flag seems wrong on restoration??

                    lda currentPly
                    sta SET_BANK_RAM;@2
                    ldx #RAMBANK_BOARD
                    stx SET_BANK_RAM;@3

                    lda@PLY savedEvaluation
                    sta Evaluation
                    lda@PLY savedEvaluation+1
                    sta Evaluation+1

                    ldx@PLY movePtr
                    ldy@PLY MoveFrom,x
                    lda@PLY restorePiece
                    sta@RAM Board,y

                    ldy@PLY MoveTo,x
                    lda@PLY capturedPiece
                    sta@RAM Board,y


    ; See if there are any 'secondary' pieces that moved
    ; here we're dealing with reverting a castling or enPassant move

                    lda@PLY secondaryPiece
                    beq .noSecondary
                    ldx@PLY secondarySquare
                    sta@RAM Board,x                     ; put piece back
                    ldy@PLY secondaryBlank
                    lda #0
                    sta@RAM Board,y                     ; blank piece origin

.noSecondary        SWAP
                    rts

You may notice the weird opcodes such as "sta@RAM..." and "lda@PLY"  -- these are in fact macros that I use to automatically manage all the odd offsets that are added to addresses when accessing RAM banks.  I have them setup for lda/sta/ldx/stx/ldy/sty. I find this very readable - it's also a great reminder where a variable actually lives.  I use this all throughout the code.

 

As to the REFER and VEND stuff, all my subroutines have this block, too.  The REFER references any subroutine that calls this one. Why?  Well, it's related to the VEND line.  just before VEND I can "declare" local variables. For example...

 

    DEF InitialisePieceSquares
    SUBROUTINE

        COMMON_VARS_MOVE
        REFER StartupBankReset ;

        VAR __initPiece, 1
        VAR __initSquare, 1
        VAR __initListPtr, 1
        
        VEND InitialisePieceSquares

 

Here we have three local variables declared. They can be used freely in this subroutine. But what if we call another subroutine that also uses local variables. How do we prevent clashes and stomping on each other?  That's where the REFER and VEND come in.  The REFER lines (there can be many) calculate the first free address in the overlay area where the local variables START.  The VEND records the end of the local variable area in the current subroutine. So any subroutine that includes "REFER InitialisePieceSquares" will start its local variables no earlier than after the "__initListPtr" variable.  I just have to maintain the REFER references, which is actually pretty simple. The green tick is just me being paranoid about that.

 

The COMMON_VARS_MOVE is just a macro declaring shared (local) variables between subroutines. Each subroutine using shared variables (for parameter passing, for example) has a macro declaring the common variables, which goes first. If there's any address changes in those variables with the multiple declarations, dasm will fire an error.

 

Here's the VAR macro which actually allocates a memory address to a temporary variable...

 


    ; Define a temporary variable for use in a subroutine
    ; Will allocate appropriate bytes, and also check for overflow of the available overlay buffer

    MAC VAR ; { name, size }
{1} = TEMPORARY_VAR
TEMPORARY_VAR SET TEMPORARY_VAR + TEMPORARY_OFFSET + {2}

OVERLAY_DELTA SET TEMPORARY_VAR - Overlay
        IF OVERLAY_DELTA > MAXIMUM_REQUIRED_OVERLAY_SIZE
MAXIMUM_REQUIRED_OVERLAY_SIZE SET OVERLAY_DELTA
        ENDIF
        IF OVERLAY_DELTA > OVERLAY_SIZE
            ECHO "Temporary Variable", {1}, "overflow!"
            ERR
        ENDIF
        LIST ON
    ENDM

It's pretty cool because it also keeps track of the largest size needed for temporary (overlay) variables, and uses THAT for the actual area declaration. So I have a sort of auto-configuring minimal size overlay area usage thing going. It all looks rather complex, but has been built up in simple steps over many iterations, and has proven extremely reliable.


Finally, that "DEF" is short for "define subroutine" and it does a whole lot of housekeeping, creating a label with the correct bank number for the subroutine and pointing the temporary variable usage pointer to the start of the overlay area. So instead of worrying about where things are and getting all the bankswitching OK, I can just do this...

                    CALL CartInit
                    CALL SetupBanks
                    CALL InitialisePieceSquares

So, the CALL macro will load and switch to the bank for the named subroutine, and then JSR to it. But it's got a bit of smarts - if the bank of the subroutine is in the same bank as the calling code (that is, switching out the current bank would be a disaster/crash) then the macro will report an error. This is a safeguard which has proven really handy.  I don't really worry about where things are - the DEF sets up the bank reference for use in the CALL macro. It all works transparently and I mostly don't have to think/worry about it.

 

So anyway, back to the timing... really early days still. I'm confident in the figures because they're just counting nodes visited and regardless of the correctness of the search... the nodes should be visited at the highest speed possible. The count is therefore an indicator of relative speed. I've spent a few long long days trying to find a bug in the search (why it was playing terribly) and even went back and did a line by line comparison of the working and non-working version. Identical.  So, much head scratching and debugging and couldn't find it. Decided to do the optimising anyway, and after moving some (overlay/shared) variables around, bango, started working much better. So, I've got (or had) something stomping on something something else is using.  That's the danger with overlays and shared variables. I am normally very careful - I have macros and code constructs to prevent this, but the one area I just modified didn't actually do this the "proper" way, and I just fixed that up. Maybe that was it. Anyway, going to do some testing before release - it's still not playing quite right.

But, bottom line... +70% speed.


Meanwhile, while we wait... here's some relaxing thinkbars (slightly newer more colourful version), which I am rather partial to...
 

 

 

Edited by Andrew Davie
  • Like 6
Link to comment
Share on other sites

12 minutes ago, Andrew Davie said:

But, bottom line... +70% speed.

Yay for more speed!  Now if only I could remember how to play chess like I did when I was younger, and not stink so much at it like I do now.  And my kingdom for square spaces on the board instead of rounded...  :P

Edited by NostAlgae37
I finalized by accident.
Link to comment
Share on other sites

3 hours ago, NostAlgae37 said:

Yay for more speed!  Now if only I could remember how to play chess like I did when I was younger, and not stink so much at it like I do now.  And my kingdom for square spaces on the board instead of rounded...  :P

Yes everyone else in the entire world aside from me, apparently, will be delighted to hear that square squares will return for all future versions.

 

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

3 minutes ago, Thomas Jentzsch said:

Very cool to hear that 3E+ became that useful. 

I think the 70% is probably over-stating the case, but I now think well over 30%. We shall see!

I'm very pleased not to have run into any bugs in the driver so far.  Is this the first example of it being used?

 

Link to comment
Share on other sites

5 minutes ago, Thomas Jentzsch said:

I know you optimized the code. Nevertheless ~30% is still cool. And then, the gained flexibility may also have allowed some of the optimization, didn't it?

 

Besides some simple test programs, this is the first real world example.

 

The 3E version was as fully optimised as I could possibly make it.

All improvements seen in 3E+ are due to the ability to have different blocks of RAM switched in at the same time. So, for accessing the board and the moves I can have board in one 'slot' and movelist in another, accessed from code in a third.  No bankswitching required during the access to these. In 3E, I had to access the RAM via 'helper' routines in the fixed bank, so a bank save to variable, jsr to access (e.g., "getboard"), switch to board bank, load variable required, restore of bank, return. This was painful and slow. Now it's just direct access and much easier. There is a bit of decision-making as to what goes where to provide the efficient use of the 'slots', but once that's done it's almost as easy as having a flat memory access. Bankswitching is incidental now, and not something I really have to worry about too much.

Edited by Andrew Davie
Link to comment
Share on other sites

I'm having great difficulty tracking down some subtle bugs, particularly with the quiescence. Moves go wonky when turned on.

I've found one or two dumb errors in the conversion - some code that got changed in the 3E version after I started the 3E+ version, and so never made it in. Goofy stuff.  I look at each reproducible error as a godsend, because if it's reproducible it's fairly easy to debug. But I'm a bit stumped, and fed up right now, with the quiescence.

 

So, here's a version (3PQ0) just to show where it's at. That is, no quiescence search and only 3 ply... it's going to play kind of badly, as it doesn't have much brains stuff happening, but much much better than the last version, due to aforementioned goof fixes.  The video shows most of a blitz game, of sorts (stella @ 300% speed).  There's a crash at the end where pawn promote is about to happen. Evidently I haven't fixed up the 3E+ conversion for that part yet!

 

 

Anyone noticed the odd ROM sizes yet?  27K?!  Courtesy the 3E+ format!


And, at this point... I really need either quiescence to start working... or a decent break.
 

chess3E+20200514_3PQ0.bin

  • Like 3
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...