Skip to content

Connect 4 & Twitter Bot #322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions Connect 4/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>

<!-- Meta -->
<meta charset="UTF-8" />
<title>Align 4 Game</title>

<!-- Styles -->
<link href="https://fonts.googleapis.com/css?family=Doppio+One" rel="stylesheet">
<link rel="stylesheet" href="styles/index.processed.css">
</head>
<body>
<div class="wrapper">
<div class="content">
<div class="sidebar">
<h1>Align 4</h1>
<div class="panel dif">
<h2>Difficulty</h2>
<div class="dif-options">
<div>
<input id="dif1" type="radio" name="dif-options" value="1">
<label for="dif1">1</label>
</div>
<div>
<input id="dif2" type="radio" name="dif-options" value="2">
<label for="dif2">2</label>
</div>
<div>
<input id="dif3" type="radio" name="dif-options" value="3" checked>
<label for="dif3">3</label>
</div>
<div>
<input id="dif4" type="radio" name="dif-options" value="4">
<label for="dif4">4</label>
</div>
<div>
<input id="dif5" type="radio" name="dif-options" value="5">
<label for="dif5">5</label>
</div>
</div>
<div class="start">
<button>Start Game</button>
</div>
</div>
<div class="panel info">
<h2></h2>
<div class="blurb"></div>
<div class="outlook"></div>
</div>
</div>
<div class="board">
<div class="lit-columns">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="lit-cells"></div>
<div class="chips"></div>
<div class="click-columns">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</div>

<!-- Scripts -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="scripts/index.js"></script>
</body>
</html>
253 changes: 253 additions & 0 deletions Connect 4/scripts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
// constants
const WEB_WORKER_URL = 'scripts/worker.js';
const BLURBS = {
'start': {
header: 'Get Ready',
blurb: 'Select your difficulty and start the game.'
},
'p1-turn': {
header: 'Your Turn',
blurb: 'Click on the board to drop your chip.'
},
'p2-turn': {
header: 'Computer\'s Turn',
blurb: 'The computer is trying to find the best way to make you lose.'
},
'p1-win': {
header: 'You Win',
blurb: 'You are a winner. Remember this moment. Carry it with you, forever.'
},
'p2-win': {
header: 'Computer Wins',
blurb: 'Try again when you\'re done wiping your tears of shame.'
},
'tie': {
header: 'Tie',
blurb: 'Everyone\'s a winner! Or loser. Depends on how you look at it.'
}
};
const OUTLOOKS = {
'win-imminent': 'Uh oh, computer is feeling saucy!',
'loss-imminent': 'Computer is unsure. Now\'s your chance!'
};

// global variables
var worker;
var currentGameState;

// document ready
$(function() {
$('.start button').on('click', startGame);
setBlurb('start');
setOutlook();

// create worker
worker = new Worker(WEB_WORKER_URL);
worker.addEventListener('message', function(e) {
switch(e.data.messageType) {
case 'reset-done':
startHumanTurn();
break;
case 'human-move-done':
endHumanTurn(e.data.coords, e.data.isWin, e.data.winningChips, e.data.isBoardFull);
break;
case 'progress':
updateComputerTurn(e.data.col);
break;
case 'computer-move-done':
endComputerTurn(e.data.coords, e.data.isWin, e.data.winningChips, e.data.isBoardFull,
e.data.isWinImminent, e.data.isLossImminent);
break;
}
}, false);
});

function setBlurb(key) {
$('.info h2').text(BLURBS[key].header);
$('.info .blurb').text(BLURBS[key].blurb);
}

function setOutlook(key) {
var $outlook = $('.info .outlook');
if(key) {
$outlook
.text(OUTLOOKS[key])
.show();
} else {
$outlook.hide();
}
}

function startGame() {
$('.dif').addClass('freeze');
$('.dif input').prop('disabled', true);
$('.lit-cells, .chips').empty();

worker.postMessage({
messageType: 'reset',
});
}

function startHumanTurn() {
setBlurb('p1-turn');
$('.click-columns div').addClass('hover');

// if mouse is already over a column, show cursor chip there
var col = $('.click-columns div:hover').index();
if(col !== -1) {
createCursorChip(1, col);
}

$('.click-columns')
.on('mouseenter', function() {
var col = $('.click-columns div:hover').index();
createCursorChip(1, col);
})
.on('mouseleave', function() {
destroyCursorChip();
});

$('.click-columns div')
.on('mouseenter', function() {
var col = $(this).index();
moveCursorChip(col);
})
.on('click', function() {
$('.click-columns, .click-columns div').off();

var col = $(this).index();
worker.postMessage({
messageType: 'human-move',
col: col
});
});
}

function endHumanTurn(coords, isWin, winningChips, isBoardFull) {
$('.click-columns div').removeClass('hover');
if(!coords) {
// column was full, human goes again
startHumanTurn();
} else {
dropCursorChip(coords.row, function() {
if(isWin) {
endGame('p1-win', winningChips);
} else if(isBoardFull) {
endGame('tie');
} else {
// pass turn to computer
startComputerTurn();
}
});
}
}

function startComputerTurn() {
setBlurb('p2-turn');

// computer's cursor chip starts far left and moves right as it thinks
createCursorChip(2, 0);

var maxDepth = parseInt($('input[name=dif-options]:checked').val(), 10) + 1;
worker.postMessage({
messageType: 'computer-move',
maxDepth: maxDepth
});
}

function updateComputerTurn(col) {
moveCursorChip(col);
}

function endComputerTurn(coords, isWin, winningChips, isBoardFull, isWinImminent, isLossImminent) {
moveCursorChip(coords.col, function() {
dropCursorChip(coords.row, function() {
if (isWin) {
endGame('p2-win', winningChips);
} else if (isBoardFull) {
endGame('tie');
} else {
if(isWinImminent) {
setOutlook('win-imminent');
} else if (isLossImminent) {
setOutlook('loss-imminent');
} else {
setOutlook();
}

// pass turn to human
startHumanTurn();
}
});
});
}

function endGame(blurbKey, winningChips) {
$('.dif').removeClass('freeze');
$('.dif input').prop('disabled', false);
setBlurb(blurbKey);
setOutlook();

if(winningChips) {
// not a tie, highlight the chips in the winning run
for(var i = 0; i < winningChips.length; i++) {
createLitCell(winningChips[i].col, winningChips[i].row);
}
}
}

function createLitCell(col, row) {
$('<div>')
.css({
'left': indexToPixels(col),
'bottom': indexToPixels(row)
})
.appendTo('.lit-cells');
}

function createCursorChip(player, col) {
var playerClass = 'p' + player;
$('<div>', { 'class': 'cursor ' + playerClass })
.css('left', indexToPixels(col))
.appendTo('.chips');

// also highlight column
$('.lit-columns div').eq(col).addClass('lit');
}

function destroyCursorChip() {
$('.chips .cursor').remove();
$('.lit-columns .lit').removeClass('lit');
}

function moveCursorChip(col, callback) {
$('.chips .cursor').css('left', indexToPixels(col));
$('.lit-columns .lit').removeClass('lit');
$('.lit-columns div').eq(col).addClass('lit');

// callback is only used when the computer is about to drop a chip
// give it a slight delay for visual interest
setTimeout(callback, 300);
}

function dropCursorChip(row, callback) {
// speed of animation depends on how far the chip has to drop
var ms = (7 - row) * 40;
var duration = (ms / 1000) + 's';

$('.chips .cursor')
.removeClass('cursor')
.css({
'bottom': indexToPixels(row),
'transition-duration': duration,
'animation-delay': duration
})
.addClass('dropped');

$('.lit-columns .lit').removeClass('lit');
setTimeout(callback, ms);
}

function indexToPixels(index) {
return (index * 61 + 1) + 'px';
}
236 changes: 236 additions & 0 deletions Connect 4/scripts/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// constants
const TOTAL_COLUMNS = 7;
const TOTAL_ROWS = 7;
const HUMAN_WIN_SCORE = -4;
const COMPUTER_WIN_SCORE = 4;
const NO_WIN_SCORE = 0;

// global variables
var currentGameState;

// game state object
var GameState = function(cloneGameState) {
this.board = [];
this.score = NO_WIN_SCORE;
this.winningChips = undefined;

// initialize an empty board
for(var col = 0; col < TOTAL_COLUMNS; col++) {
this.board[col] = [];
}

// clone from existing game state (if one was passed in)
if(cloneGameState) {
for (var col = 0; col < TOTAL_COLUMNS; col++) {
for (var row = 0; row < cloneGameState.board[col].length; row++) {
this.board[col][row] = cloneGameState.board[col][row];
}
}
this.score = cloneGameState.score;
}
}
GameState.prototype.makeMove = function(player, col) {
var coords = undefined;
var row = this.board[col].length;
if (row < TOTAL_ROWS) {
this.board[col][row] = player;
this.setScore(player, col, row);
coords = { col: col, row: row };
}
return coords;
}
GameState.prototype.isBoardFull = function() {
for (var col = 0; col < TOTAL_COLUMNS; col++) {
if (this.board[col].length < TOTAL_ROWS) {
// found an unfilled column
return false;
}
}
return true;
}
GameState.prototype.setScore = function(player, col, row) {
var isWin =
this.checkRuns(player, col, row, 0, 1) || // vertical
this.checkRuns(player, col, row, 1, 0) || // horizontal
this.checkRuns(player, col, row, 1, 1) || // diagonal "/"
this.checkRuns(player, col, row, 1, -1) // diagonal "\"

if(isWin) {
this.score = (player === 1) ? HUMAN_WIN_SCORE : COMPUTER_WIN_SCORE;
} else {
this.score = NO_WIN_SCORE;
}
}
GameState.prototype.checkRuns = function(player, col, row, colStep, rowStep) {
var runCount = 0;

// check from 3 chips before to 3 chips after the specified chip
// this covers all possible runs of 4 chips that include the specified chip
for (var step = -3; step <= 3; step++) {
if (this.getPlayerForChipAt(col + step * colStep, row + step * rowStep) === player) {
runCount++;
if (runCount === 4) {
// winning run, step backwards to find the chips that make up this run
this.winningChips = [];
for(var backstep = step; backstep >= step - 3; backstep--) {
this.winningChips.push({
col: col + backstep * colStep,
row: row + backstep * rowStep
});
}
return true;
}
} else {
runCount = 0;
if(step === 0) {
// no room left for a win
break;
}
}
}

// no winning run found
return false;
}
GameState.prototype.getPlayerForChipAt = function(col, row) {
var player = undefined;
if (this.board[col] !== undefined && this.board[col][row] !== undefined) {
player = this.board[col][row];
}
return player;
}
GameState.prototype.isWin = function() {
return (this.score === HUMAN_WIN_SCORE || this.score === COMPUTER_WIN_SCORE);
}

// listen for messages from the main thread
self.addEventListener('message', function(e) {
switch(e.data.messageType) {
case 'reset':
resetGame();
break;
case 'human-move':
makeHumanMove(e.data.col);
break;
case 'computer-move':
makeComputerMove(e.data.maxDepth);
break;
}
}, false);

function resetGame() {
currentGameState = new GameState();

self.postMessage({
messageType: 'reset-done'
});
}

function makeHumanMove(col) {
// coords is undefined if the move is invalid (column is full)
var coords = currentGameState.makeMove(1, col);
var isWin = currentGameState.isWin();
var winningChips = currentGameState.winningChips;
var isBoardFull = currentGameState.isBoardFull();
self.postMessage({
messageType: 'human-move-done',
coords: coords,
isWin: isWin,
winningChips: winningChips,
isBoardFull: isBoardFull
});
}

function makeComputerMove(maxDepth) {
var col;
var isWinImminent = false;
var isLossImminent = false;
for (var depth = 0; depth <= maxDepth; depth++) {
var origin = new GameState(currentGameState);
var isTopLevel = (depth === maxDepth);

// fun recursive AI stuff kicks off here
var tentativeCol = think(origin, 2, depth, isTopLevel);
if (origin.score === HUMAN_WIN_SCORE) {
// AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth
// this solves the "apathy" problem
isLossImminent = true;
break;
} else if (origin.score === COMPUTER_WIN_SCORE) {
// AI knows how to win, no need to think deeper, use this move
// this solves the "cocky" problem
col = tentativeCol;
isWinImminent = true;
break;
} else {
// go with this move, for now at least
col = tentativeCol;
}
}

var coords = currentGameState.makeMove(2, col);
var isWin = currentGameState.isWin();
var winningChips = currentGameState.winningChips;
var isBoardFull = currentGameState.isBoardFull();
self.postMessage({
messageType: 'computer-move-done',
coords: coords,
isWin: isWin,
winningChips: winningChips,
isBoardFull: isBoardFull,
isWinImminent: isWinImminent,
isLossImminent: isLossImminent
});
}

function think(node, player, recursionsRemaining, isTopLevel) {
var scoreSet = false;
var childNodes = [];

// consider each column as a potential move
for (var col = 0; col < TOTAL_COLUMNS; col++) {
if(isTopLevel) {
self.postMessage({
messageType: 'progress',
col: col
});
}

// make sure column isn't already full
var row = node.board[col].length;
if (row < TOTAL_ROWS) {
// create new child node to represent this potential move
var childNode = new GameState(node);
childNode.makeMove(player, col);
childNodes[col] = childNode;

if(!childNode.isWin() && recursionsRemaining > 0) {
// no game stopping win and there are still recursions to make, think deeper
var nextPlayer = (player === 1) ? 2 : 1;
think(childNode, nextPlayer, recursionsRemaining - 1);
}

if (!scoreSet) {
// no best score yet, just go with this one for now
node.score = childNode.score;
scoreSet = true;
} else if (player === 1 && childNode.score < node.score) {
// assume human will always pick the lowest scoring move (least favorable to computer)
node.score = childNode.score;
} else if (player === 2 && childNode.score > node.score) {
// computer should always pick the highest scoring move (most favorable to computer)
node.score = childNode.score;
}
}
}

// collect all moves tied for best move and randomly pick one
var candidates = [];
for (var col = 0; col < TOTAL_COLUMNS; col++) {
if (childNodes[col] != undefined && childNodes[col].score === node.score) {
candidates.push(col);
}
}
var moveCol = candidates[Math.floor(Math.random() * candidates.length)];
return moveCol;
}
236 changes: 236 additions & 0 deletions Connect 4/styles/index.processed.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
html {
display: table;
width: 100%;
height: 100%;
}

body {
display: table-row;
background: #000 radial-gradient(1000px 500px, #3f546b, #000);
}

.wrapper {
display: table-cell;
vertical-align: middle;
text-align: center;
}

.content {
display: inline-block;
width: 668px;
margin: 0 auto;
padding: 10px 20px;
}

.sidebar {
float: left;
margin-right: 20px;
width: 220px;
text-align: left;
font-family: 'Doppio One', sans-serif;
color: #ccc;
}

h1,
h2 {
color: #fff;
margin: 0;
font-weight: normal;
}

h1 {
height: 68px;
line-height: 68px;
font-size: 40px;
text-align: right;
}

h2 {
font-size: 18px;
}

.panel {
padding: 12px;
margin-bottom: 20px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}

.dif-options {
clear: both;
overflow: hidden;
margin: 20px -7px 0;
}

.dif-options div {
float: left;
width: 20%;
}

.dif-options input {
display: none;
}

.dif-options input:checked+label {
color: #fff;
background-color: #593f6b;
border-color: #fff;
cursor: default;
}

.dif-options label {
display: block;
margin: 0 auto;
width: 24px;
height: 24px;
background-color: #666;
border: solid 2px #ccc;
border-radius: 8px;
color: #999;
text-align: center;
line-height: 24px;
cursor: pointer;
}

.freeze .dif-options input:not(:checked)+label {
font-size: 0;
margin: 7px auto;
width: 10px;
height: 10px;
border-radius: 4px;
color: transparent;
line-height: 10px;
cursor: default;
transition: .2s;
}

.start {
margin-top: 20px;
}

.start button {
display: block;
width: 100%;
padding: 2px 12px 4px;
font-family: inherit;
font-size: 24px;
border: solid 2px #ccc;
border-radius: 8px;
background-color: #593f6b;
color: #fff;
cursor: pointer;
}

.start button:focus {
outline: none;
}

.freeze .start {
display: none;
}

.info div {
margin-top: 10px;
}

.board {
position: relative;
float: left;
width: 428px;
height: 428px;
margin-top: 68px;
background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/board.png");
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.4);
}

.lit-columns,
.lit-cells,
.chips,
.click-columns {
position: absolute;
width: 428px;
height: 428px;
}

.lit-columns div {
float: left;
width: 60px;
height: 426px;
margin: 1px 0 1px 1px;
transition: background-color 0.1s;
}

.lit-columns .lit {
background-color: rgba(255, 255, 255, 0.1);
transition: background-color 0.1s;
}

.lit-cells div {
position: absolute;
width: 60px;
height: 60px;
background-color: rgba(255, 255, 255, 0.3);
}

.chips div {
position: absolute;
width: 60px;
height: 60px;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}

.chips .p1 {
background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/p1-chip.png");
}

.chips .p2 {
background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/p2-chip.png");
}

.chips .cursor {
bottom: 428px;
transition: left 0.1s ease-out;
}

.chips .dropped {
transition: bottom ease-in;
animation: bounce 0.3s;
}

.click-columns div {
float: left;
width: 61px;
height: 428px;
}

.click-columns div:first-child {
padding-left: 1px;
}

.click-columns .hover {
cursor: pointer;
}

@keyframes bounce {
0% {
animation-timing-function: ease-out;
transform: translateY(0);
}
30% {
animation-timing-function: ease-in;
transform: translateY(-40px);
}
60% {
animation-timing-function: ease-out;
transform: translateY(0);
}
80% {
animation-timing-function: ease-in;
transform: translateY(-10px);
}
100% {
animation-timing-function: ease-out;
transform: translateY(0);
}
}
223 changes: 223 additions & 0 deletions Connect 4/styles/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
$asset-path: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/';

html {
display: table;
width: 100%;
height: 100%;
}

body {
display: table-row;
background: #000 radial-gradient(1000px 500px, #3f546b, #000);
}

.wrapper {
display: table-cell;
vertical-align: middle;
text-align: center;
}

.content {
display: inline-block;
width: 668px;
margin: 0 auto;
padding: 10px 20px;
}

.sidebar {
float: left;
margin-right: 20px;
width: 220px;
text-align: left;
font-family: 'Doppio One', sans-serif;
color: #ccc;
}

h1, h2 {
color: #fff;
margin: 0;
font-weight: normal;
}

h1 {
height: 68px;
line-height: 68px;
font-size: 40px;
text-align: right;
}

h2 {
font-size: 18px;
}

.panel {
padding: 12px;
margin-bottom: 20px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}

.dif-options {
clear: both;
overflow: hidden;
margin: 20px -7px 0;

div {
float: left;
width: 20%;
}

input {
display: none;

&:checked + label {
color: #fff;
background-color: #593f6b;
border-color: #fff;
cursor: default;
}
}

label {
display: block;
margin: 0 auto;
width: 24px;
height: 24px;
background-color: #666;
border: solid 2px #ccc;
border-radius: 8px;
color: #999;
text-align: center;
line-height: 24px;
cursor: pointer;
}
}

.freeze .dif-options input:not(:checked) + label {
font-size: 0;
margin: 7px auto;
width: 10px;
height: 10px;
border-radius: 4px;
color: transparent;
line-height: 10px;
cursor: default;
transition: .2s;
}

.start {
margin-top: 20px;

button {
display: block;
width: 100%;
padding: 2px 12px 4px;
font-family: inherit;
font-size: 24px;
border: solid 2px #ccc;
border-radius: 8px;
background-color: #593f6b;
color: #fff;
cursor: pointer;

&:focus {
outline: none;
}
}
}

.freeze .start {
display: none;
}

.info div {
margin-top: 10px;
}

.board {
position: relative;
float: left;
width: 428px;
height: 428px;
margin-top: 68px;
background-image: url($asset-path + 'board.png');
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.4);
}

.lit-columns, .lit-cells, .chips, .click-columns {
position: absolute;
width: 428px;
height: 428px;
}

.lit-columns {
div {
float: left;
width: 60px;
height: 426px;
margin: 1px 0 1px 1px;
transition: background-color 0.1s;
}

.lit {
background-color: rgba(255, 255, 255, 0.1);
transition: background-color 0.1s;
}
}

.lit-cells div {
position: absolute;
width: 60px;
height: 60px;
background-color: rgba(255, 255, 255, 0.3);
}

.chips {
div {
position: absolute;
width: 60px;
height: 60px;
backface-visibility: hidden;
}

.p1 {
background-image: url($asset-path + 'p1-chip.png');
}

.p2 {
background-image: url($asset-path + 'p2-chip.png');
}

.cursor {
bottom: 428px;
transition: left 0.1s ease-out;
}
.dropped {
transition: bottom ease-in;
animation: bounce 0.3s;
}
}

.click-columns {
div {
float: left;
width: 61px;
height: 428px;

&:first-child {
padding-left: 1px;
}
}

.hover {
cursor: pointer;
}
}

@keyframes bounce {
0% { animation-timing-function: ease-out; transform: translateY(0); }
30% { animation-timing-function: ease-in; transform: translateY(-40px); }
60% { animation-timing-function: ease-out; transform: translateY(0); }
80% { animation-timing-function: ease-in; transform: translateY(-10px); }
100% { animation-timing-function: ease-out; transform: translateY(0); }
}
75 changes: 75 additions & 0 deletions Twitter Bot/twitterbot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import tweepy
import schedule
import time
import random
import logging

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()

# Twitter API credentials (Replace with your own)
API_KEY = 'GTTmtNd2LSDhNnOYzqiQtoClL'
API_SECRET_KEY = 'MmRHVDcpcqATVAFAcs5Nn39f1LsKGsynoL91a5DpNEaQmK1JQE'
ACCESS_TOKEN = '1827621926421852160-NjQcEEWBd18axdaG8tLI5J2Xcfxskw'
ACCESS_TOKEN_SECRET = 'IdbqCSoNzyRbyTKHfjebWCmOFQkpVxnmqZmpCNF8lUToz'
BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAPcbwAEAAAAAud%2BrAHqJZ7j%2FwSYTsv%2FsT8sTML4%3DXqyeqOUrhx7nH04uKm3gQehNYiAy8Hbq07FCpgoQBQiBWBhUvc'

# Authenticate with Twitter using Bearer Token for API v2
def authenticate_twitter():
try:
client = tweepy.Client(bearer_token=BEARER_TOKEN,
consumer_key=API_KEY,
consumer_secret=API_SECRET_KEY,
access_token=ACCESS_TOKEN,
access_token_secret=ACCESS_TOKEN_SECRET)
logger.info("Authentication successful")
return client
except Exception as e:
logger.error("Unexpected error during authentication", exc_info=True)
raise e

# List of motivational quotes
QUOTES = [
"Believe you can and you're halfway there. – Theodore Roosevelt",
"Your limitation—it’s only your imagination.",
"Push yourself, because no one else is going to do it for you.",
"Great things never come from comfort zones.",
"Dream it. Wish it. Do it.",
"Success doesn’t just find you. You have to go out and get it.",
"The harder you work for something, the greater you’ll feel when you achieve it.",
"Don’t stop when you’re tired. Stop when you’re done.",
"Wake up with determination. Go to bed with satisfaction.",
"Do something today that your future self will thank you for."
]

# Function to post a tweet
def post_tweet(client):
try:
quote = random.choice(QUOTES)
client.create_tweet(text=quote) # Using create_tweet method in v2
logger.info(f"Tweeted: {quote}")
except Exception as e:
logger.error("An unexpected error occurred while posting tweet", exc_info=True)

def main():
client = authenticate_twitter()

# Test posting a tweet manually to ensure it works before scheduling
post_tweet(client) # Comment this out if you want to test scheduling only

# Schedule the tweet to post every day at a specific time
schedule.every().day.at("09:00").do(post_tweet, client=client)

# For debugging, post a tweet every 1 minute (for testing)
schedule.every(1).minutes.do(post_tweet, client=client)

logger.info("Bot is running...")

while True:
schedule.run_pending()
time.sleep(1)

# Main entry point
if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -164,6 +164,8 @@ <h1 class="text-uppercase Heading"> <span class="badge">small javascript project
<a href="./Wekipidia_clone/index.html" class="hvr-sweep-to-right projectsName">Wikipedia clone</a>
<a href="./pairsgame/index.html" class="hvr-sweep-to-right projectsName">Pairs Game</a>
<a href="./Checkers/index.html" class="hvr-sweep-to-right projectsName">Checkers</a>
<a href="./Connect 4/index.html" class="hvr-sweep-to-right projectsName">Connect 4</a>
<a href="./Twitter Bot/twitterbot.py" class="hvr-sweep-to-right projectsName">Twitter Bot</a>
</section>

<!-- Bootstrap Core JS -->