├── tictactoe ├── __init__.py ├── __main__.py ├── tictactoe.py └── app.py ├── requirements-dev.txt ├── .gitignore ├── screenshot.png ├── setup.py ├── tests └── test_game.py └── README.md /tictactoe/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | briefcase>=0.1.6 2 | voc==0.1.4 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | android 2 | build 3 | .gradle 4 | *.class 5 | *.jar 6 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasdorneles/tictactoe-voc/HEAD/screenshot.png -------------------------------------------------------------------------------- /tictactoe/__main__.py: -------------------------------------------------------------------------------- 1 | from tictactoe.app import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='tictactoe', 6 | version='0.0.1', 7 | packages=['tictactoe'], 8 | options={ 9 | 'app': { 10 | 'formal_name': 'TicTacToe', 11 | 'bundle': 'org.pybee.demo', 12 | } 13 | } 14 | ) 15 | -------------------------------------------------------------------------------- /tests/test_game.py: -------------------------------------------------------------------------------- 1 | from tictactoe import tictactoe 2 | import unittest 3 | 4 | 5 | class GameTest(unittest.TestCase): 6 | def setUp(self): 7 | # called before every test 8 | self.game = tictactoe.Game() 9 | def test_create_game(self): 10 | self.assertIsNotNone(self.game) 11 | def test_play(self): 12 | self.game.play(0,0) 13 | # self.assertEquals(self.game.board[0][0], 'O') 14 | self.game.play(1,0) 15 | self.game.play(1,1) 16 | self.game.play(2,0) 17 | with self.assertRaisesRegexp(KeyError, "Winner"): 18 | self.game.play(2,2) 19 | def test_play_twice(self): 20 | self.game.play(0,0) 21 | with self.assertRaisesRegexp(KeyError, "Invalid"): 22 | self.game.play(0,0) 23 | def test_full_board(self): 24 | self.game.play(0,0) 25 | self.game.play(1,0) 26 | self.game.play(2,0) 27 | self.game.play(1,1) 28 | self.game.play(0,1) 29 | self.game.play(0,2) 30 | self.game.play(2,1) 31 | self.game.play(2,2) 32 | with self.assertRaisesRegexp(KeyError, "Tie"): 33 | self.game.play(1,2) 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tic Tac Toe 2 | =========== 3 | 4 | This is a native Android app written in Python, using the [BeeWare](http://pybee.org/) suite. 5 | 6 | The Python code is compiled to Java bytecode using [VOC](http://pybee.org/voc), 7 | and the Android APK is packaged using [briefcase](https://github.com/pybee/briefcase). 8 | 9 | **Requirements** 10 | 11 | * JDK 12 | * Android SDK 13 | 14 | ## How to run 15 | 16 | Ensure that you have [Android SDK](https://developer.android.com/studio/index.html#downloads) installed. 17 | 18 | Install the Python development requirements: 19 | 20 | pip install -r requirements-dev.txt 21 | 22 | [Plug in your Android device](https://developer.android.com/training/basics/firstapp/running-app.html) or [start an emulator](https://developer.android.com/studio/run/emulator-commandline.html). 23 | 24 | Build and run the app: 25 | 26 | python setup.py android 27 | (cd android && ./gradlew run) 28 | 29 | ![Game screenshot](screenshot.png) 30 | 31 | 32 | ## Want to know more? 33 | 34 | If you liked this, you might like: 35 | 36 | * [Native Android drawing app written in Python](https://github.com/eliasdorneles/drawingapp-voc) 37 | * [Template for native Android apps written in Python](https://github.com/eliasdorneles/beeware-android-template) 38 | * [Video of a talk explaining how VOC works](https://www.youtube.com/watch?v=9c4DEYIXYCM) 39 | -------------------------------------------------------------------------------- /tictactoe/tictactoe.py: -------------------------------------------------------------------------------- 1 | def cycle(seq): 2 | full = list(seq) 3 | while True: 4 | for i in full: 5 | yield i 6 | 7 | 8 | class Game: 9 | def __init__(self): 10 | self.board = [[None] * 3 for _ in range(3)] 11 | self.turns = cycle('OX') 12 | self._switch_player() 13 | self.game_over = False 14 | 15 | def _switch_player(self): 16 | self.current_player = next(self.turns) 17 | 18 | def _has_winner(self): 19 | def is_equal_row(row): 20 | return all(row) and len(set(row)) == 1 21 | 22 | inverted_board = [ 23 | [line[0] for line in self.board], 24 | [line[1] for line in self.board], 25 | [line[2] for line in self.board], 26 | ] 27 | return any([ 28 | any(is_equal_row(line) for line in self.board), 29 | any(is_equal_row(line) for line in inverted_board), 30 | is_equal_row([self.board[0][0], self.board[1][1], self.board[2][2]]), 31 | is_equal_row([self.board[2][0], self.board[1][1], self.board[0][2]]) 32 | ]) 33 | 34 | def _is_full(self): 35 | return all(all(line) for line in self.board) 36 | 37 | def play(self, row, column): 38 | # XXX: using KeyError because of VOC#352 39 | if self.game_over or self.board[row][column]: 40 | raise KeyError("Invalid play") 41 | self.board[row][column] = self.current_player 42 | if self._has_winner(): 43 | self.game_over = True 44 | raise KeyError("Winner: " + self.current_player) 45 | elif self._is_full(): 46 | self.game_over = True 47 | raise KeyError("Tie!") 48 | self._switch_player() 49 | 50 | def print(self): 51 | for line in self.board: 52 | print(' '.join(line)) 53 | -------------------------------------------------------------------------------- /tictactoe/app.py: -------------------------------------------------------------------------------- 1 | import android 2 | from android.util import Log 3 | from android.widget import LinearLayout 4 | from android.widget import Button 5 | from android.widget import TextView 6 | from android.view import Gravity 7 | import android.view 8 | from tictactoe import tictactoe 9 | 10 | 11 | class ButtonClick(implements=android.view.View[OnClickListener]): 12 | def __init__(self, callback, *args, **kwargs): 13 | self.callback = callback 14 | self.args = args 15 | self.kwargs = kwargs 16 | 17 | def onClick(self, view: android.view.View) -> void: 18 | self.callback(*self.args, **self.kwargs) 19 | 20 | 21 | class MyApp: 22 | def __init__(self): 23 | self._activity = android.PythonActivity.setListener(self) 24 | self.buttons = [] 25 | self.top_label = None 26 | self.game = tictactoe.Game() 27 | self.message = None 28 | 29 | def onCreate(self): 30 | def create_button_row(): 31 | return [ 32 | Button(self._activity), 33 | Button(self._activity), 34 | Button(self._activity), 35 | ] 36 | 37 | self.buttons = [ 38 | create_button_row(), 39 | create_button_row(), 40 | create_button_row(), 41 | ] 42 | 43 | vlayout = LinearLayout(self._activity) 44 | vlayout.setOrientation(LinearLayout.VERTICAL) 45 | 46 | self.top_label = TextView(self._activity) 47 | self.top_label.setTextSize(50) 48 | vlayout.addView(self.top_label) 49 | 50 | for row in self.buttons: 51 | hlayout = LinearLayout(self._activity) 52 | hlayout.setOrientation(LinearLayout.HORIZONTAL) 53 | hlayout.setGravity(Gravity.CENTER) 54 | for button in row: 55 | button.setTextSize(50) 56 | hlayout.addView(button) 57 | 58 | vlayout.addView(hlayout) 59 | 60 | for i in range(3): 61 | for j in range(3): 62 | self.buttons[i][j].setOnClickListener(ButtonClick(self.play, i, j)) 63 | 64 | self.restart_button = Button(self._activity) 65 | self.restart_button.setText('Restart') 66 | self.restart_button.setOnClickListener(ButtonClick(self.restart_game)) 67 | vlayout.addView(self.restart_button) 68 | 69 | footer = TextView(self._activity) 70 | footer.setText('Powered by Python') 71 | footer.setGravity(Gravity.CENTER) 72 | vlayout.addView(footer) 73 | 74 | self.updateUI() 75 | 76 | self._activity.setContentView(vlayout) 77 | 78 | def restart_game(self): 79 | self.game = tictactoe.Game() 80 | self.message = None 81 | self.updateUI() 82 | 83 | def updateUI(self): 84 | if self.message: 85 | self.top_label.setText(self.message) 86 | else: 87 | self.top_label.setText('Player: ' + self.game.current_player) 88 | for i, row in enumerate(self.buttons): 89 | for j, button in enumerate(row): 90 | thing = self.game.board[i][j] 91 | if thing: 92 | button.setText(thing) 93 | else: 94 | button.setText(' ') 95 | 96 | def play(self, i, j): 97 | if self.game.game_over: 98 | return 99 | 100 | print('going to play game', self.game, ' in position:', i, j) 101 | try: 102 | self.game.play(i, j) 103 | self.message = None 104 | except KeyError as e: 105 | message = str(e)[1:-1] 106 | print('message', message) 107 | self.message = message 108 | self.updateUI() 109 | 110 | 111 | def main(): 112 | MyApp() 113 | --------------------------------------------------------------------------------