Commit 9cc6f36e authored by Oskar Skog's avatar Oskar Skog

0.5.6: Works on Windows with CPython and windows-curses

parent cce56daf
2019-03-09 Oskar Skog <https://oskog97.com/#contact>
0.5.6
* anonymine_engine.py (game_engine):
Bugfix: Fails to initialize fields in non-unix
mode. It was supposed to work without fork (although slowly),
but apparently it never has.
Bugfix: Removed tempfile without closing first.
* anonymine.py (curses_game): Add a curses_voodoo attribute to decide
whether or not to reset to shell mode while initializing. It's
needed on at least Debian, but makes the game break on Windows.
* anonymine.py: Cleaned up the imports section.
* anonymine.py (highscores_display): Use more if less fails.
* cursescfg, anonymine.py (curses_game.input): ^C and keypad
enter did not work in curses mode on Windows. Also added a quit
key "q".
* windows-beta/*, anonymine.py [Windows]: New location for configuration
files and such.
* INSTALL.Windows.txt: (created)
* README (platforms): It was time for an update on that section.
* FAQ: Using Cygwin does not seem so convenient anymore.
2018-11-07 Oskar Skog <https://oskog97.com/#contact>
0.5.5
* anonymine_engine.py: Use SIGTERM instead of SIGCONT to kill workers.
......@@ -27,7 +48,7 @@
2017-09-04 Oskar Skog <https://oskog97.com/#contact>
pre-0.5.2
* anonymine.py (convert_param): Small correction to the easter egg.
* configure.py (find_MODUKLES): Fixed Mac specific bug.
* configure.py (find_MODULES): Fixed Mac specific bug.
* configure.py: Corrected spelling in the shebang.
* INSTALL: Mac comment, one small fix.
......
......@@ -11,16 +11,6 @@ What are the losers' highscores?
an unlosable game.
Both the count of mines left and the time are factors for the rankings.
Is Cygwin required for the Windows version?
Actually no, but it's convenient:
- Most of the installation procedure use the pre-existing
./configure && make && make install
- Without fork(2), it will take a few times longer to initialize
a minefield.
- Without Cygwin, Python would require a separate curses module.
It should work on Python for Windows with a manually installed curses
module.
Can I change the colors?
Yes, the configuration file /etc/anonymine/cursescfg defines the key
bindings and colors. If the in-file documentation is not sufficient,
......
......@@ -44,6 +44,7 @@ Those files exist in $(builddir) while all other files are in $(srcdir).
* mkenginecfg.py Used internally by mkenginecfg
* reconfigure For remaking Makefile; (Generated by ./configure)
test.py Misc small unnecessary functions: demos, etc
windows-beta/ For Windows: cfg files, tmp files and highscores
? enginecfg.user is a file that you can create manually if you want to.
enginecfg.user MUST be created in $(builddir), not $(srcdir).
......
Extract the Anonymine directory and place it wherever you want.
Download Python from https://www.python.org/
Launch the installer and make sure to select "Add Python x.y to PATH".
Open up a command prompt and run the following commands:
pip install --upgrade pip
pip install windows-curses
"anonymine.py" should now run when opened.
If you want shortcuts, you'll have to make them yourself.
Installation is now complete.
You can stop reading now.
Planning automation
===================
https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe
https://docs.python.org/3.7/using/windows.html
python-3.7.0.exe /quiet PrependPath=1 Include_pip=1 Shortcuts=0
......@@ -15,7 +15,7 @@
NAME := anonymine
NAME_C := Anonymine
DESCRIPTION = Minesweeper without guessing
VERSION := 0.5.5
VERSION := 0.5.6
# Four more variables are required: sysconfdir, vargamesdir, EXECUTABLES and
# MODULES
......
......@@ -61,25 +61,54 @@ Software & hardware requirements
Tested platforms
================
CPython 2.6.4 is lowest version of Python that has been tested.
Platform Notes
-------- -----
Cygwin on NT 6.1 [Py 2] Unstudied resizing bug.
Debian 7
Debian 8 [Py 2, 3] Host system, well tested
FreeBSD 9.2 [Py 2]
FreeBSD 10 [Py 2] ssh anonymine-demo.oskog97.com:2222
Haiku nightly Terminal bug fixed in hrev50662
Mac OS X [Py 2] ICNS icon is untested
Minix 3.3 [Py 2] Issues with the highscores: rounding, no pager
NetBSD 6.1 [Py 2] TERM=xterm almost works, vt100 is default by sys
OpenBSD 5.8 [Py 2, 3] Failed to pipe to less on Python 3
OpenSUSE 12.2 [Py 2, 3]
Trisquel 6.0 [Py 2, 3] Old host system, old versions well tested
OpenIndiana dev [Py 2.6] Anonymine 0.1.17 to 0.2.29 won't work
OpenIndiana Hipster Works great [Py 2.7]
Python version
==============
2.6 has been tested, but that was a long time ago. Anonymine version
0.2.30 should work fine.
Python 2.7 and 3.x are both being actively tested, there should not be
any issues whatsoever.
It works with both CPython and PyPy. PyPy3 is being actively tested.
Operating system
================
unix-like
---------
Should work flawlessly on most GNU(/Linux) distributions.
MacOS needs more testing. Especially to make sure the installation
goes smoothly.
Previous versions have been tested to work on various GNU/Linux
distributions, FreeBSD, OpenBSD, NetBSD, DragonflyBSD, Minix 3
and OpenIndiana.
It also runs on Haiku. It does not install, read INSTALL.Haiku.
Windows
-------
Versions before 0.5.6 will not work on Windows, but newer versions
do provided that you have installed windows-curses from PyPI. This
probably only works with CPython. Initializing minefields take
longer as there is no `os.fork`.
0.5.6 does not have an automatic installer, read the instructions
in INSTALL.Windows.txt.
There is also a version that runs on Cygwin avaiable on
<https://gitlab.com/oskog97/anonymine-windows>. It's faster but
takes a while to install and Cygwin takes up half a gigabyte of
storage. It also has a few issues with shortcuts and installing
for all users.
Goals
=====
......@@ -90,5 +119,6 @@ Goals
0.3 High-scores
0.4 Meta file clean ups (eg. README)
0.5 Mouse support
0.6 Illustrative documentation for the solver algorithm
0.6 Windows installer that is not a PITA
0.7 Illustrative documentation for the solver algorithm
#!/usr/bin/python
# Copyright (c) Oskar Skog, 2016-2018
# Copyright (c) Oskar Skog, 2016-2019
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
......@@ -26,7 +26,7 @@
'''A minesweeper that can be solved without guessing
Copyright (c) Oskar Skog, 2016-2017
Copyright (c) Oskar Skog, 2016-2019
Released under the FreeBSD license.
A minesweeper that can be solved without guessing
......@@ -48,35 +48,32 @@ import os
import sys
import errno
import locale
import signal
import sys
import traceback # Not required.
# Let piping to less(1) fail on Minix.
try:
import subprocess
except:
# Let piping to less(1) fail on Minix.
pass
# Allow module names to be changed later.
import anonymine_engine as game_engine
# argparse: Losing the ability to take command line options is no biggy.
# traceback: Not needed unless shit happens.
import traceback # Not required.
# argparse is new in 2.7 and 3.2.
# Losing the ability to take command line options is no biggy.
try:
import argparse
except:
pass
import signal
if 'SIGTSTP' in dir(signal):
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
import anonymine_engine as game_engine
# These two are still needed.
GAME_NAME = 'Anonymine'
GAME_FILENAME = GAME_NAME.lower().replace(' ', '-')
GAME_FILENAME = 'anonymine'
GAME_CRAPTEXT = """Anonymine version MAKEFILE_GAME_VERSION
Copyright (c) Oskar Skog, 2016-2017
Copyright (c) Oskar Skog, 2016-2019
Released under the Simplified BSD license (2 clause).
\n"""
......@@ -271,6 +268,9 @@ class curses_game():
WARNING: This does not leave curses mode on exceptions!
'''
self.curses_voodoo = not sys.platform.startswith('win')
# Constants
self.travel_diffs = {
'square': {
......@@ -531,7 +531,7 @@ class curses_game():
direction_keys = self.direction_keys['hex']
else:
direction_keys = self.direction_keys['square']
look_for = ['reveal', 'flag', 'toggle-attention'] + direction_keys
look_for = ['reveal','flag','toggle-attention','quit']+direction_keys
# Receive input from player.
ch = self.window.getch()
# Interpret.
......@@ -563,16 +563,20 @@ class curses_game():
pre_game = engine.game_status == 'pre-game'
if pre_game:
self.message('Initializing field... This may take a while.')
curses.reset_shell_mode() # BUG: see comments above __init__
if self.curses_voodoo:
curses.reset_shell_mode() #BUG: see comments above __init__
engine.reveal(self.cursor)
if pre_game:
curses.reset_prog_mode() # BUG: see comments above __init__
if self.curses_voodoo:
curses.reset_prog_mode() #BUG: see comments above __init__
# Clear junk that gets on the screen from impatient players.
self.window.redrawwin()
elif command in direction_keys:
self.travel(engine.field, command)
elif command == 'toggle-attention':
self.attention_mode = not self.attention_mode
elif command == 'quit': # Needed on Windows
raise KeyboardInterrupt
elif ch != curses.KEY_MOUSE:
# Don't do this all the time, that'd be a little wasteful.
self.window.redrawwin()
......@@ -1478,6 +1482,7 @@ def highscores_display(title, headers, rows, cfgfile):
break
except UnicodeEncodeError:
continue
# Pipe to (less || more)
try:
os.environ['LESSSECURE'] = '1' # LOL
less = subprocess.Popen(
......@@ -1487,11 +1492,20 @@ def highscores_display(title, headers, rows, cfgfile):
less.communicate(text)
less.wait()
except:
output(sys.stderr, 'Failed to pipe to `less -S -# 1`!\n')
if sys.version_info[0] == 3:
output(sys.stdout, utext)
else:
output(sys.stdout, text)
try:
more = subprocess.Popen(['more'], stdin=subprocess.PIPE)
more.communicate(text)
more.wait()
except:
output(sys.stderr,
'Failed to pipe to `less -S -"#" {}` and `more`!\n'.format(
hs_conf['less_step']
)
)
if sys.version_info[0] == 3:
output(sys.stdout, utext)
else:
output(sys.stdout, text)
def play_game(parameters):
......@@ -1601,6 +1615,7 @@ def main():
"MAKEFILE_CFGDIR" + '/' + cfgfile,
sys.prefix + '/etc/' + GAME_FILENAME + '/' + cfgfile,
'/etc/' + GAME_FILENAME + '/' + cfgfile,
'windows-beta/' + cfgfile,
)
for location in locations:
try:
......
#!/usr/bin/python
# Copyright (c) Oskar Skog, 2016, 2018
# Copyright (c) Oskar Skog, 2016-2019
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
......@@ -27,7 +27,7 @@
'''This module provides the engine of Anonymine
Copyright (c) Oskar Skog, 2016
Copyright (c) Oskar Skog, 2016-2019
Released under the FreeBSD license.
The engine of Anonymine
......@@ -704,19 +704,26 @@ class game_engine():
for x, y in mines:
f.write('{0} {1}\n'.format(x, y))
f.close()
# FUNCTION STARTS HERE.
# Clean up after whatever may have called us.
self.field.clear()
unix = True
if 'fork' not in dir(os):
unix = False
unix = 'fork' in dir(os)
# Need to know tempfile name before creating children because
# we're going to add 64 bits of entropy just in case exclusive
# opening somehow would fail to stop a tempfile exploit.
filename = self.cfg['init-field']['filename']
for i in range(8):
filename += '-'+str(ord(os.urandom(1)))
# Create slaves.
# 1. Create slaves (unix)
# 2. Set up alarm
# 3. Fork error (not unix)
# 4. Wait for slaves to finish (unix)
# 5. Enter mine coordinates
# 1: Create slaves.
if unix:
children = []
for i in range(self.cfg['init-field']['procs']):
......@@ -736,7 +743,8 @@ class game_engine():
except KeyboardInterrupt:
# Kill the python interpreter on ^C.
os._exit(1)
# Security timeout raises Exception
# 2: Security timeout raises Exception
if 'alarm' in dir(signal): # (unix only)
def die(ignore1, ignore2):
raise security_alert
......@@ -748,53 +756,60 @@ class game_engine():
def stop_alarm():
pass
security_timeout = False
# Compatibility for non-unix systems, or on fork failure.
# 3: Compatibility for non-unix systems, or on fork failure.
if not unix:
try:
child()
success_pid = os.getpid()
except security_alert:
security_timeout = True
stop_alarm()
return
# Wait for the first child to finish.
success_pid = 0
try:
while success_pid not in children:
while True:
try:
pid, status = os.wait()
except OSError as e:
if 'EINTR' in dir(errno):
if e.errno == errno.EINTR:
continue
raise
except InterruptedError:
continue
break
if os.WIFEXITED(status):
success_pid = pid
except security_alert:
security_timeout = True
stop_alarm()
# Kill all remaining children.
# Delete potential left-over tempfiles.
for child in children:
if child != success_pid:
try:
os.remove(filename.format(child))
except OSError:
# File does not exist, process has not finished.
# 4: Wait for the first child to finish.
if unix:
success_pid = 0
try:
while success_pid not in children:
while True:
try:
pid, status = os.wait()
except OSError as e:
if 'EINTR' in dir(errno):
if e.errno == errno.EINTR:
continue
raise
except InterruptedError:
continue
break
if os.WIFEXITED(status):
success_pid = pid
except security_alert:
security_timeout = True
stop_alarm()
# Kill all remaining children.
# Delete potential left-over tempfiles.
for child in children:
if child != success_pid:
try:
os.kill(child, signal.SIGTERM)
os.waitpid(child, 0) # Destroy the zombie.
os.remove(filename.format(child))
except OSError:
pass
# File does not exist, process has not finished.
try:
os.kill(child, signal.SIGTERM)
os.waitpid(child, 0) # Destroy the zombie.
os.remove(filename.format(child))
except OSError:
pass
# 5: Done soplving the field, enter the mine locations:
if security_timeout:
raise security_alert('Initialization took too long, aborted')
# Parse the tempfile.
self.field.clear()
f = open(filename.format(success_pid))
lines = f.read().split('\n')[:-1]
f.close()
os.remove(filename.format(success_pid))
mines = []
for line in lines:
......@@ -827,6 +842,17 @@ class game_engine():
self.field.clear()
self.field.fill(mines)
self.field.reveal(startpoint)
def win(field, engine):
engine.game_status = 'game-won'
field.set_callback('win', None, None)
field.set_callback('lose', None, None)
def lose(field, engine):
engine.game_status = 'game-lost'
field.set_callback('win', None, None)
field.set_callback('lose', None, None)
self.field.set_callback('win', win, self)
self.field.set_callback('lose', lose, self)
def flag(self, coordinate):
'''Automatic flag/unflag at `coordinate`.
......@@ -857,20 +883,10 @@ class game_engine():
See the doc-string for the class as a whole for (more)
information.
'''
def win(field, engine):
engine.game_status = 'game-won'
field.set_callback('win', None, None)
field.set_callback('lose', None, None)
def lose(field, engine):
engine.game_status = 'game-lost'
field.set_callback('win', None, None)
field.set_callback('lose', None, None)
self.field.clear()
# Enter the main loop.
self.start = time.time()
self.field.set_callback('win', win, self)
self.field.set_callback('lose', lose, self)
while self.game_status in ('pre-game', 'play-game'):
interface.output(self)
interface.input(self)
......
......@@ -56,9 +56,11 @@
'curses-input': {
# Key bindings:
# Only ASCII characters are supported in the key bindings.
'flag': ['f', ],
'reveal': [' ', '\n', ],
'toggle-attention': ['!', '?', ],
# (3 = ^C and 459 = numpad enter on Windows)
'quit': ['q', 3, ],
'flag': ['f', ],
'reveal': [' ', '\n', '\r', 459,],
'toggle-attention': ['!', '?', ],
# Hexagonal direction numbers:
# 5 0
# 4 1
......
This diff is collapsed.
#!/usr/bin/python
#@not-modified@
# Remove the above line if this file has been modified.
# If you don't, this file will be overwritten.
{
'init-field': {
#'procs': 1, # Default is to not overload.
'filename': 'windows-beta/mines.{0}',
'sec-maxtime': 1200,
'sec-maxarea': 2500, # Crash if a huge field is requested.
},
'hiscores': {
'file': 'windows-beta/highscores.txt',
'maxsize': 524288,
'entries': 16,
'use-user': True,
'use-nick': True,
'nick-maxlen': 100,
},
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment