News:

This is the TEST SITE - feel free to post but content will be deleted

Main Menu

Slim's Geek Gazette

Started by Slim, August 07, 2025, 01:53:07 PM

Previous topic - Next topic

Slim

A bit less than 24 hours after I started, I think this is finished ..

mmfin.pngHighslide Image Viewer

The ✘ is a "no pegs" feedback, it just gives the player a confirmation that the row has been entered and he/she is onto the next one. The top row is the secret code, it appears at the end of the game - either when the player runs out of goes, or gets it right.

Quite happy with that, didn't take very long though. Suppose I'll write some brief documentation and put it on GitHub.

Slim

I've discovered that there was a "deluxe" edition that used five-peg codes (rather than four), twelve goes (rather than ten) and eight colours to choose from (rather than six).

So I can get a bit more fun out of this project by including a "deluxe" mode - the script should be easy to modify so that the various numbers are variables rather than static. The extra colours will be orange and purple (orange and brown in the original, but they look a bit similar on a terminal and of course the B for Brown is already used by Blue. For this reason I'm using N for Noir to enter a black peg).

So the program will be run by

$ mastermind.sh -d
To invoke deluxe mode.


Slim

So, I did that. Nice exercise, but didn't take long.

mmd.pngHighslide Image Viewer

Slim


Slim

I also added a "minimal mode". Three peg codes, four colours, eight goes. Very quick mod.

 minim.pngHighslide Image Viewer

Slim

A bit of spreadsheet fun this morning. I've set up a sheet with waypoints on my favourite long distance bike ride to the edge of Norfolk and back.

waypointsskm.pngHighslide Image Viewer

So the first column is the waypoint name, the second is the distance from the previous waypoint, the third is the cumulative distance (so that, for example, Pode Hole is 107.98 km from home) and the last column is the time of day I'm due to arrive there, based on overall progress of 16 km / hour. The start time (03:00 here) is entered manually.

The third and fourth columns are calculated from the second. The nice thing about doing it in a spreadsheet is that if you have to plan a detour into the route due to roadworks, you just have to adjust a figure in the second column and the rest will recalculate. You can also do "what if" based on different start times, of course.

Next I need to get the spreadsheet to extrapolate the waypoints, distances and times for the way back.

Slim

It's nice to have your entire bike ride history in Strava and to be able to look through your old tracks, but the user interface is a bit cumbersome - especially if you have a lot of activities in your history.

Fortunately - Strava allows you to download all of your activity history in a big old zip file. So I thought I'd do that, and organise them as a sort of local database on my own PC.

So I downloaded them. The Strava servers just keep whatever you upload unsurprisingly, so I found that I had track files in a number of different formats - mostly GPX, but also FIT, TCX and BIN. File outputs from different devices. My first job then was to convert them to the same format, for a uniform dataset. I decided to convert them to GPX, firstly because it's closest to a standard and secondly because GPX files can be inspected in a text editor.

There's a free Linux command line tool that converts GPS tracks between different formats so I knocked up a quick script to convert the TCX, FIT and BIN files to GPX. That left me with about 1500 GPX files.

The plan is to organise them into folders by month, and perhaps by year as well.

The programming language Python has some nifty functions for interrogating GPX files so I'll use that. It's years since I coded in Python so I'll get ChatGPT to help.

Should keep me off the streets for a bit.

Slim

So: my idea is to to set the files up into different folder "views", so for example I might have a folder for each month since 2015, I might have folders representing rough ride distances and so on, using the data encoded in the GPX files. I can do this without duplicating the data around, using hard links.

Python has some nifty add-ons for the GPX format but it's years since I coded in that language and I'm not familiar with its GPX facilities. So that's where ChatGPT comes in. Could you please knock me up a utility to examine a GPX file and determine whether it contains a waypoint east of a particular longitude? I asked. Handy for finding longer rides that go into Lincolnshire. Two seconds later:

#!/usr/bin/env python3
import sys
import xml.etree.ElementTree as ET

if len(sys.argv) != 3:
print("Usage: gpx_east_of.py <longitude_threshold> <file.gpx>")
sys.exit(2)

threshold = float(sys.argv[1])
path = sys.argv[2]

try:
root = ET.parse(path).getroot()
except Exception:
sys.exit(1)

for el in root.iter():
if isinstance(el.tag, str) and el.tag.endswith(("trkpt", "rtept", "wpt")):
lon = el.attrib.get("lon")
if lon is not None and float(lon) > threshold:
sys.exit(0)

sys.exit(1)

Tested it, worked first time. Then I thought of a refinement, so I asked it for a Python utility that would determine whether a ride had passed through a particular point on the map. Every GPX waypoint has a latitude and longitude parameter, represented as a floating point number like this:

<trkpt lat="52.674767850" lon="-1.435571909">

I decided on a tolerance of 100 metres. GPS tracking devices are considerably more accurate than that, but they only record at intervals. 100m is more than good enough for this purpose anyway. Again, took a couple of seconds:

#!/usr/bin/env python3
import sys
import xml.etree.ElementTree as ET

if len(sys.argv) != 4:
print("Usage: gpx_near_30m.py <target_lat> <target_lon> <file.gpx>")
sys.exit(2)

target_lat = float(sys.argv[1])
target_lon = float(sys.argv[2])
path = sys.argv[3]

lat_tol = 0.0009
lon_tol = 0.0015

try:
root = ET.parse(path).getroot()
except Exception:
sys.exit(1)

for el in root.iter():
if isinstance(el.tag, str) and el.tag.endswith(("trkpt", "rtept", "wpt")):
lat = el.attrib.get("lat")
lon = el.attrib.get("lon")
if lat is not None and lon is not None:
lat = float(lat)
lon = float(lon)

if (abs(lat - target_lat) <= lat_tol and
abs(lon - target_lon) <= lon_tol):
sys.exit(0)

sys.exit(1)


You can see the 100m tolerance assigned as lat_tol and lon_tol. Longitude tolerance is actually a little bit more complicated than this, because the precise figure representing 100m differs depending on latitude. But for anywhere in England, that 0.0015 value will be close enough.

Slim

I decided, for my next coding project, to create a Bash, terminal version of the old game Battleships, originally played using pencil and paper, occasionally sold as a plastic or wooden board game, but realised as a computer game in the late '70s.

It's basically grid-based so similar to Star Trek in some respects. But it involves some complex problem-solving, so it should keep me occupied for a few weeks.

Slim

So I've now written the function that populates a 10x10 grid with "ships" - One 5 squares in length, one length 4, two length 3, one length 2.

The algorithm I came up with is as follows:

For each ship:
Pick a square at random. Pick a direction at random (up, right, down, left).
Try to fit the ship in that direction from the starting square.
If it won't go (goes out of bounds or collides with another ship), try the next direction.

If all four directions are exhausted, add 9 to the square's "address" in the grid and try again (this loops round, I use modular arithmetic).

Because 9 is coprime with 100 then (as any fule kno) you're bound to visit all of the available squares eventually.

I tested this by populating a grid 400,000 times and there was never a situation in which a ship couldn't be fitted so I'm happy that it must necessarily always work.

I suspect that most programmers wouldn't use this method. A decent way to do it would be: pick a square at random and a direction at random. Attempt to fit the ship. Doesn't fit? pick another random square and direction; repeat until all ships are fitted. Nice and simple and that would work but I just have a dislike of "do random till it works" methods.

At the moment the output looks like this. Very crude; this is just a diagnostic output to demonstrate that it works.

.  .  .  5  .  .  .  .  .  . 
.  .  .  5  .  .  .  .  .  . 
.  .  .  5  .  4  .  .  .  . 
.  .  .  5  .  4  .  .  .  . 
.  .  .  5  .  4  .  .  .  . 
.  .  .  .  .  4  23 23 23 2 
.  13 .  .  .  .  .  .  .  2 
.  13 .  .  .  .  .  .  .  . 
.  13 .  .  .  .  .  .  .  . 
.  .  .  .  .  .  .  .  .  .

Note that I'm using 23 and 13 for the 3-length ships, to distinguish them from each other. Using these numbers I can derive the length of the vessel using modulus 10. I could use a separate array for the lengths. Arguably that would be more elegant. But having learned to code on a ZX81 I'm stingy with memory.

Slim

  
The board will look something like this during play, although this is a mock-up. The ⬜ are where the player (or computer) has missed. The coloured squares are the player's ships. The ⬛⬛ is a dead ship, the 🔥🔥🔥 is a ship with three hits.

  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦⬜🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🔥🔥🔥🟦🟦
  🟦🟦⬜🟦🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦⬜🟦⬛⬛🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦

  🟪🟪🟪🟪🟪🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦⬜🟦
  🟦🟦⬜🟦🟦🟦🟨🟦🟦🟦
  🟦🟦🟦🟦🟦🟦🟨🟦🟦🟦
  🟦🟦🟧🟧🟧🟦🟨🟦🟩🟦
  🟦🟦🟦🟦🟦🟦🟨🟦🟩🟦
  🟦🟦🟦⬜🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🟧🟧🟧🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
  🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦

Slim

I spent hours on this yesterday, it went very well and I have a working prototype.

🟦🟦⬜🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦⬜🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦⬜🟦🟦🟦
🟦🟦🟦⬜🟦🟦🟦🟦⬜🟦
🟦🟦🟦⬛🟦🟦🟦🟦🟦🟦
🟦🟦🟦⬛🟦🟦🟦🟦🟦🟦
🟦🟦⬜⬛⬜⬜🟦⬜🟦🟦
🟦🟦🟦⬛🟦🟦🟦🟦🟦⬜
🟦🟦🟦🟦🟦🟦🟦⬜🟦🟦
🟦🟦🟦🟦⬜⬛⬛⬛🟦🟦

🟦⬜🟦🟦🟦🟩🟦⬜🟦🟦
🟦🟦🟦🟦🟦🟩⬜🟦🟦🟦
🟦🟦🟦⬜🟦🟩🟪🟪🟪🟪
🟦🟦🟦⬜🟦🟩🟦🟦🟦🟦
⬜🟦🟦⬜🟦🟩🟦⬜🟦🟦
🟦🟦🟦🟦🟦🟦🟦🟦⬜🟦
🟧🟧🟧🟦🟦🟦⬜🟦🟦⬜
🟦🟦🟨🟦🟦🟦🟦🟦⬜🟦
🟦⬜🟨🟦🟦🟦🟦⬜🟦🟦
🟦⬜🟦⬜⬜🟦⬜🟦🟦🟦

At the moment the computer just takes random potshots on its turn, it just finds a square it hasn't already tried and takes aim.

What I need to do is to introduce a sort of crude AI in which, after it hits a ship, it will take shots at adjacent squares, horizontally or vertically until it's sunk the player's ship, then go back to opportunistic hunting.

I think I'll also have a rule that the code can't place ships that touch each other, except diagonally. I also need a counter for ships sunk so it can work out when someone's won, although that's trivial.


Slim

I've written it in Bash so I'm calling it ..

bash-ships.avifHighslide Image Viewer

Slim

I modified the code so that it only places ships that don't touch horizontally or vertically - and that actually makes the computer opponent logic much simpler, because if you have two hits in adjacent squares, they must be part of the same ship. This has been really absorbing to do. Got a feeling the whole thing will be complete in a few days.

So I need some sort of AI spectrum for the computer's logic. Obviously the player has to work it out for him/herself. There will be:

  • just random hunting
  • hit a ship, no idea of orientation or extent - shoot adjacent squares until I get a hit
  • hit two squares, now I know the orientation - shoot an unhit square that's adjacent in the same orientation to a hit square until I register a "sunk". Then back to 1.

That 3rd idea makes it relatively simple I think - it doesn't have to work out "I've gone as far as I can to the right, now to move left". It just has to aim for unhit, adjacent squares on the same vertical or horizontal.

Thenop

Quote from: Slim on April 12, 2026, 12:14:19 PMI've written it in Bash so I'm calling it ..

bash-ships.avifHighslide Image Viewer

Liking the image especially seeing the playing field in the binoculars lenses.

Fingers are still a bit of an AI miss thoigh. Also: he has a pipe sticking out of his jaw?

(And I'd almost go as far as suggesting to do a play on the ships positioning with the SH in both words you have for the name)